Skip to content

feat: Add pauseable context container #21

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<PauseableContainerProps> {
static propTypes = {
children: PropTypes.node.isRequired,
shouldUpdate: PropTypes.bool.isRequired,
};

shouldComponentUpdate(nextProps: PauseableContainerProps): boolean {
return nextProps.shouldUpdate;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string>;
let TestConsumer: React.FC;
beforeEach(() => {
TestContext = React.createContext('default');
TestConsumer = () => {
const valueFromContext = useContext(TestContext);
return <span>{valueFromContext}</span>;
};
});

it('allows updates', () => {
const { rerender } = render(
<TestContext.Provider value="one">
<PauseableContextContainer Context={TestContext} shouldUpdate={true}>
<TestConsumer />
</PauseableContextContainer>
</TestContext.Provider>,
);

expect(screen.queryByText('one')).toBeInTheDocument();

// "one" -> "two"
rerender(
<TestContext.Provider value="two">
<PauseableContextContainer Context={TestContext} shouldUpdate={true}>
<TestConsumer />
</PauseableContextContainer>
</TestContext.Provider>,
);

expect(screen.queryByText('one')).not.toBeInTheDocument();
expect(screen.queryByText('two')).toBeInTheDocument();
});

it('prevents updates', () => {
const { rerender } = render(
<TestContext.Provider value="one">
<PauseableContextContainer Context={TestContext} shouldUpdate={false}>
<TestConsumer />
</PauseableContextContainer>
</TestContext.Provider>,
);

expect(screen.queryByText('one')).toBeInTheDocument();

// "one" -> "two"
rerender(
<TestContext.Provider value="two">
<PauseableContextContainer Context={TestContext} shouldUpdate={false}>
<TestConsumer />
</PauseableContextContainer>
</TestContext.Provider>,
);

expect(screen.queryByText('one')).toBeInTheDocument();
expect(screen.queryByText('two')).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -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<any>;
}

const PauseableContextContainer: React.FC<PauseableContextContainerProps> = (props) => {
const { children, Context, shouldUpdate } = props;

const currentValue = useContext(Context);
const lastAllowedValueRef = useRef(currentValue);
if (shouldUpdate) {
lastAllowedValueRef.current = currentValue;
}

return <Context.Provider value={lastAllowedValueRef.current}>{children}</Context.Provider>;
};

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;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
4 changes: 4 additions & 0 deletions packages/react-pauseable-containers/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
3 changes: 3 additions & 0 deletions packages/react-pauseable-containers/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { ReactNode } from 'react';

export interface PauseableContainerProps {
children: ReactNode;
shouldUpdate: boolean;
}
Original file line number Diff line number Diff line change
@@ -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 (
<Typography variant="body1" component="div">
count: <Chip label={count} />
<RenderCount />
</Typography>
);
};

export default ContextDemoItem;
export { DemoContext };
Original file line number Diff line number Diff line change
Expand Up @@ -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<PauseableContainerWrapperProps> = (props) => {
const { PauseableContainer, children, ...allOtherProps } = props;
const { PauseableContainer, children, initialState = true, ...allOtherProps } = props;

const [shouldUpdate, setShouldUpdate] = useState(false);
const [shouldUpdate, setShouldUpdate] = useState(initialState);

return (
<Paper style={{ marginTop: 10, padding: 5 }}>
Expand Down
3 changes: 3 additions & 0 deletions packages/react-pauseable-containers/stories/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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',
Expand All @@ -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), []);
Expand Down Expand Up @@ -58,15 +68,78 @@ export const PauseableComponentContainerStory = (): ReactNode => {
<PauseableContainerWrapper PauseableContainer={PauseableComponentContainer}>
<ComponentDemoItem count={count} />
</PauseableContainerWrapper>
<PauseableContainerWrapper PauseableContainer={PauseableComponentContainer}>
<PauseableContainerWrapper
PauseableContainer={PauseableComponentContainer}
initialState={false}
>
<ComponentDemoItem count={count} />
</PauseableContainerWrapper>
</div>
);
};
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 (
<DemoContext.Provider value={count}>
<div>
<Typography variant="h4">
<code>&lt;PauseableContextContainer&gt;</code>
</Typography>
<Typography variant="subtitle1">
The <code>count</code> value is put into a context, which each child reads from.
</Typography>
<Typography variant="subtitle1">
Each child is wrapped in a <code>PauseableContextContainer</code> whose{' '}
<code>shouldUpdate</code> prop is controlled by the checkbox.
</Typography>
<Button onClick={increment} variant="contained">
Increment
</Button>

<div>
Value in context:
<Chip label={count} />
</div>

<PauseableContainerWrapper
PauseableContainer={PauseableContextContainer}
Context={DemoContext}
>
<ContextDemoItem />
</PauseableContainerWrapper>
<PauseableContainerWrapper
PauseableContainer={PauseableContextContainer}
Context={DemoContext}
>
<ContextDemoItem />
</PauseableContainerWrapper>
<PauseableContainerWrapper
PauseableContainer={PauseableContextContainer}
Context={DemoContext}
initialState={false}
>
<ContextDemoItem />
</PauseableContainerWrapper>
</div>
</DemoContext.Provider>
);
};
PauseableContextContainerStory.storyName = 'PauseableContextContainer';

/* PauseableComponentContainer
* One control sets and displays Redux state, each child reads from Redux.
*/

export const PauseableReduxContainerStory = () => {
return (
<div>
<Typography variant="h4">
Expand All @@ -88,16 +161,12 @@ const PauseableReduxContainerDemo = () => {
<PauseableContainerWrapper PauseableContainer={PauseableReduxContainer}>
<ReduxDemoItem />
</PauseableContainerWrapper>
<PauseableContainerWrapper PauseableContainer={PauseableReduxContainer}>
<PauseableContainerWrapper PauseableContainer={PauseableReduxContainer} initialState={false}>
<ReduxDemoItem />
</PauseableContainerWrapper>
</div>
);
};

export const PauseableReduxContainerStory = (): ReactNode => {
return <PauseableReduxContainerDemo />;
};
PauseableReduxContainerStory.storyName = 'PauseableReduxContainer';
PauseableReduxContainerStory.decorators = [reduxDecorator];

Expand Down