diff --git a/packages/react-pauseable-containers/src/PauseableComponentContainer.tsx b/packages/react-pauseable-containers/src/PauseableComponentContainer.tsx index 38f092f..dbb5284 100644 --- a/packages/react-pauseable-containers/src/PauseableComponentContainer.tsx +++ b/packages/react-pauseable-containers/src/PauseableComponentContainer.tsx @@ -1,9 +1,15 @@ +import PropTypes from 'prop-types'; import React, { ReactNode } from 'react'; import { PauseableContainerProps } from './types'; // This is based on https://github.com/reactjs/react-static-container/ -- but with types class PauseableComponentContainer extends React.Component { + static propTypes = { + children: PropTypes.node.isRequired, + shouldUpdate: PropTypes.bool.isRequired, + }; + shouldComponentUpdate(nextProps: PauseableContainerProps): boolean { return nextProps.shouldUpdate; } diff --git a/packages/react-pauseable-containers/src/PauseableContextContainer.test.tsx b/packages/react-pauseable-containers/src/PauseableContextContainer.test.tsx new file mode 100644 index 0000000..fac0805 --- /dev/null +++ b/packages/react-pauseable-containers/src/PauseableContextContainer.test.tsx @@ -0,0 +1,67 @@ +/* eslint-env jest */ +import '@testing-library/jest-dom/extend-expect'; +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; + +import { PauseableContextContainer } from '.'; +import { Context, useContext } from 'react'; + +describe('PauseableContextContainer', () => { + let TestContext: Context; + let TestConsumer: React.FC; + beforeEach(() => { + TestContext = React.createContext('default'); + TestConsumer = () => { + const valueFromContext = useContext(TestContext); + return {valueFromContext}; + }; + }); + + it('allows updates', () => { + const { rerender } = render( + + + + + , + ); + + expect(screen.queryByText('one')).toBeInTheDocument(); + + // "one" -> "two" + rerender( + + + + + , + ); + + expect(screen.queryByText('one')).not.toBeInTheDocument(); + expect(screen.queryByText('two')).toBeInTheDocument(); + }); + + it('prevents updates', () => { + const { rerender } = render( + + + + + , + ); + + expect(screen.queryByText('one')).toBeInTheDocument(); + + // "one" -> "two" + rerender( + + + + + , + ); + + expect(screen.queryByText('one')).toBeInTheDocument(); + expect(screen.queryByText('two')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/react-pauseable-containers/src/PauseableContextContainer.tsx b/packages/react-pauseable-containers/src/PauseableContextContainer.tsx new file mode 100644 index 0000000..0eb5b1c --- /dev/null +++ b/packages/react-pauseable-containers/src/PauseableContextContainer.tsx @@ -0,0 +1,29 @@ +import propTypes from 'prop-types'; +import React, { Context, useContext, useRef } from 'react'; + +import { PauseableContainerProps } from './types'; + +export interface PauseableContextContainerProps extends PauseableContainerProps { + Context: Context; +} + +const PauseableContextContainer: React.FC = (props) => { + const { children, Context, shouldUpdate } = props; + + const currentValue = useContext(Context); + const lastAllowedValueRef = useRef(currentValue); + if (shouldUpdate) { + lastAllowedValueRef.current = currentValue; + } + + return {children}; +}; + +PauseableContextContainer.propTypes = { + // @TODO: Trying to replicate the Consumer/Producer shape in propTypes doesn't play nice with InferProps + Context: propTypes.any.isRequired, + children: propTypes.node.isRequired, + shouldUpdate: propTypes.bool.isRequired, +}; + +export default PauseableContextContainer; diff --git a/packages/react-pauseable-containers/src/PauseableReduxContainer.tsx b/packages/react-pauseable-containers/src/PauseableReduxContainer.tsx index f955cc2..498a3fb 100644 --- a/packages/react-pauseable-containers/src/PauseableReduxContainer.tsx +++ b/packages/react-pauseable-containers/src/PauseableReduxContainer.tsx @@ -7,7 +7,6 @@ import { createPauseableStore, PauseableStoreInstance } from 'redux-pauseable-st import { PauseableContainerProps } from './types'; export interface PauseableReduxContainerProps extends PauseableContainerProps { - children: React.ReactNode; dispatchWhenPaused?: boolean | null; } diff --git a/packages/react-pauseable-containers/src/index.ts b/packages/react-pauseable-containers/src/index.ts index 67fee22..bef4953 100644 --- a/packages/react-pauseable-containers/src/index.ts +++ b/packages/react-pauseable-containers/src/index.ts @@ -1,5 +1,9 @@ export { default as PauseableComponentContainer } from './PauseableComponentContainer'; export * from './PauseableComponentContainer'; + +export { default as PauseableContextContainer } from './PauseableContextContainer'; +export * from './PauseableContextContainer'; + export { default as PauseableReduxContainer } from './PauseableReduxContainer'; export * from './PauseableReduxContainer'; diff --git a/packages/react-pauseable-containers/src/types.ts b/packages/react-pauseable-containers/src/types.ts index 72281b9..645177f 100644 --- a/packages/react-pauseable-containers/src/types.ts +++ b/packages/react-pauseable-containers/src/types.ts @@ -1,3 +1,6 @@ +import { ReactNode } from 'react'; + export interface PauseableContainerProps { + children: ReactNode; shouldUpdate: boolean; } diff --git a/packages/react-pauseable-containers/stories/helpers/ContextDemoItem.tsx b/packages/react-pauseable-containers/stories/helpers/ContextDemoItem.tsx new file mode 100644 index 0000000..5309b25 --- /dev/null +++ b/packages/react-pauseable-containers/stories/helpers/ContextDemoItem.tsx @@ -0,0 +1,21 @@ +import React, { useContext } from 'react'; +import Chip from '@material-ui/core/Chip'; +import Typography from '@material-ui/core/Typography'; + +import { RenderCount } from 'react-hibernate-dev-helpers'; + +const DemoContext = React.createContext(0); + +const ContextDemoItem: React.FC = () => { + const count = useContext(DemoContext); + + return ( + + count: + + + ); +}; + +export default ContextDemoItem; +export { DemoContext }; diff --git a/packages/react-pauseable-containers/stories/helpers/PauseableContainerWrapper.tsx b/packages/react-pauseable-containers/stories/helpers/PauseableContainerWrapper.tsx index b8b3a98..efa7f1e 100644 --- a/packages/react-pauseable-containers/stories/helpers/PauseableContainerWrapper.tsx +++ b/packages/react-pauseable-containers/stories/helpers/PauseableContainerWrapper.tsx @@ -6,15 +6,17 @@ import Paper from '@material-ui/core/Paper'; export interface PauseableContainerWrapperProps { PauseableContainer: ReactComponentLike; + initialState?: boolean; + [unrecognizedProp: string]: any; } /** * Provides a standard interface for demoing the shouldUpdateprop */ const PauseableContainerWrapper: React.FC = (props) => { - const { PauseableContainer, children, ...allOtherProps } = props; + const { PauseableContainer, children, initialState = true, ...allOtherProps } = props; - const [shouldUpdate, setShouldUpdate] = useState(false); + const [shouldUpdate, setShouldUpdate] = useState(initialState); return ( diff --git a/packages/react-pauseable-containers/stories/helpers/index.ts b/packages/react-pauseable-containers/stories/helpers/index.ts index 6559ba7..6459656 100644 --- a/packages/react-pauseable-containers/stories/helpers/index.ts +++ b/packages/react-pauseable-containers/stories/helpers/index.ts @@ -2,5 +2,8 @@ export { default as PauseableContainerWrapper } from './PauseableContainerWrappe export { default as ComponentDemoItem } from './ComponentDemoItem'; +export { default as ContextDemoItem } from './ContextDemoItem'; +export { DemoContext } from './ContextDemoItem'; + export { default as ReduxDemoItem } from './ReduxDemoItem'; export { default as ReduxStateDisplay } from './ReduxStateDisplay'; diff --git a/packages/react-pauseable-containers/stories/reactPauseableContainers.stories.tsx b/packages/react-pauseable-containers/stories/reactPauseableContainers.stories.tsx index 62b38eb..2d78348 100644 --- a/packages/react-pauseable-containers/stories/reactPauseableContainers.stories.tsx +++ b/packages/react-pauseable-containers/stories/reactPauseableContainers.stories.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useCallback, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import Button from '@material-ui/core/Button'; import Chip from '@material-ui/core/Chip'; @@ -9,12 +9,18 @@ import 'typeface-roboto'; import { reduxDecorator } from 'react-hibernate-dev-helpers'; import { - ComponentDemoItem, PauseableContainerWrapper, + ComponentDemoItem, + ContextDemoItem, + DemoContext, ReduxDemoItem, ReduxStateDisplay, } from './helpers'; -import { PauseableComponentContainer, PauseableReduxContainer } from '../src'; +import { + PauseableComponentContainer, + PauseableContextContainer, + PauseableReduxContainer, +} from '../src'; export default { title: 'React Pauseable Containers', @@ -26,7 +32,11 @@ export default { }, }; -export const PauseableComponentContainerStory = (): ReactNode => { +/* PauseableComponentContainer + * Set/update state in parent and pass it down to children via props + */ + +export const PauseableComponentContainerStory = () => { const [count, setCount] = useState(0); const increment = useCallback(() => setCount((n) => n + 1), []); @@ -58,7 +68,10 @@ export const PauseableComponentContainerStory = (): ReactNode => { - + @@ -66,7 +79,67 @@ export const PauseableComponentContainerStory = (): ReactNode => { }; PauseableComponentContainerStory.storyName = 'PauseableComponentContainer'; -const PauseableReduxContainerDemo = () => { +/* PauseableContextContainer + * Set/update state in parent and pass it down to children via context + */ + +export const PauseableContextContainerStory = () => { + const [count, setCount] = useState(0); + + const increment = useCallback(() => setCount((n) => n + 1), []); + + return ( + +
+ + <PauseableContextContainer> + + + The count value is put into a context, which each child reads from. + + + Each child is wrapped in a PauseableContextContainer whose{' '} + shouldUpdate prop is controlled by the checkbox. + + + +
+ Value in context: + +
+ + + + + + + + + + +
+
+ ); +}; +PauseableContextContainerStory.storyName = 'PauseableContextContainer'; + +/* PauseableComponentContainer + * One control sets and displays Redux state, each child reads from Redux. + */ + +export const PauseableReduxContainerStory = () => { return (
@@ -88,16 +161,12 @@ const PauseableReduxContainerDemo = () => { - +
); }; - -export const PauseableReduxContainerStory = (): ReactNode => { - return ; -}; PauseableReduxContainerStory.storyName = 'PauseableReduxContainer'; PauseableReduxContainerStory.decorators = [reduxDecorator];