Skip to content

Commit

Permalink
feat: add createReducerContext and createStateContext factories
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich authored Feb 4, 2020
2 parents 3533efc + 63f3a60 commit 84b8310
Show file tree
Hide file tree
Showing 12 changed files with 694 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
- [**State**](./docs/State.md)
- [`createMemo`](./docs/createMemo.md) — factory of memoized hooks.
- [`createReducer`](./docs/createReducer.md) — factory of reducer hooks with custom middleware.
- [`createReducerContext`](./docs/createReducerContext.md) and [`createStateContext`](./docs/createStateContext.md) — factory of hooks for a sharing state between components.
- [`useDefault`](./docs/useDefault.md) — returns the default value when state is `null` or `undefined`.
- [`useGetSet`](./docs/useGetSet.md) — returns state getter `get()` instead of raw state.
- [`useGetSetState`](./docs/useGetSetState.md) — as if [`useGetSet`](./docs/useGetSet.md) and [`useSetState`](./docs/useSetState.md) had a baby.
Expand Down
91 changes: 91 additions & 0 deletions docs/createReducerContext.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# `createReducerContext`

Factory for react context hooks that will behave just like [React's `useReducer`](https://reactjs.org/docs/hooks-reference.html#usereducer) except the state will be shared among all components in the provider.

This allows you to have a shared state that any component can update easily.

## Usage

An example with two counters that shared the same value.

```jsx
import { createReducerContext } from 'react-use';

type Action = 'increment' | 'decrement';

const reducer = (state: number, action: Action) => {
switch (action) {
case 'increment':
return state + 1;
case 'decrement':
return state - 1;
default:
throw new Error();
}
};

const [useSharedCounter, SharedCounterProvider] = createReducerContext(reducer, 0);

const ComponentA = () => {
const [count, dispatch] = useSharedCounter();
return (
<p>
Component A &nbsp;
<button type="button" onClick={() => dispatch('decrement')}>
-
</button>
&nbsp;{count}&nbsp;
<button type="button" onClick={() => dispatch('increment')}>
+
</button>
</p>
);
};

const ComponentB = () => {
const [count, dispatch] = useSharedCounter();
return (
<p>
Component B &nbsp;
<button type="button" onClick={() => dispatch('decrement')}>
-
</button>
&nbsp;{count}&nbsp;
<button type="button" onClick={() => dispatch('increment')}>
+
</button>
</p>
);
};

const Demo = () => {
return (
<SharedCounterProvider>
<p>Those two counters share the same value.</p>
<ComponentA />
<ComponentB />
</SharedCounterProvider>
);
};
```

## Reference

```jsx
const [useSharedState, SharedStateProvider] = createReducerContext(reducer, initialState);

// In wrapper
const Wrapper = ({ children }) => (
// You can override the initial state for each Provider
<SharedStateProvider initialState={overrideInitialState}>
{ children }
</SharedStateProvider>
)

// In a component
const Component = () => {
const [sharedState, dispatch] = useSharedState();

// ...
}
```
68 changes: 68 additions & 0 deletions docs/createStateContext.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# `createStateContext`

Factory for react context hooks that will behave just like [React's `useState`](https://reactjs.org/docs/hooks-reference.html#usestate) except the state will be shared among all components in the provider.

This allows you to have a shared state that any component can update easily.

## Usage

An example with a shared text between two input fields.

```jsx
import { createStateContext } from 'react-use';

const [useSharedText, SharedTextProvider] = createStateContext('');

const ComponentA = () => {
const [text, setText] = useSharedText();
return (
<p>
Component A:
<br />
<input type="text" value={text} onInput={ev => setText(ev.target.value)} />
</p>
);
};

const ComponentB = () => {
const [text, setText] = useSharedText();
return (
<p>
Component B:
<br />
<input type="text" value={text} onInput={ev => setText(ev.target.value)} />
</p>
);
};

const Demo = () => {
return (
<SharedTextProvider>
<p>Those two fields share the same value.</p>
<ComponentA />
<ComponentB />
</SharedTextProvider>
);
};
```

## Reference

```jsx
const [useSharedState, SharedStateProvider] = createStateContext(initialValue);

// In wrapper
const Wrapper = ({ children }) => (
// You can override the initial value for each Provider
<SharedStateProvider initialValue={overrideInitialValue}>
{ children }
</SharedStateProvider>
)

// In a component
const Component = () => {
const [sharedState, setSharedState] = useSharedState();

// ...
}
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"@storybook/addon-notes": "5.3.9",
"@storybook/addon-options": "5.3.9",
"@storybook/react": "5.3.9",
"@testing-library/react": "^9.4.0",
"@testing-library/react-hooks": "3.2.1",
"@types/jest": "25.1.1",
"@types/react": "16.9.11",
Expand Down
26 changes: 26 additions & 0 deletions src/createReducerContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createFactory, createContext, useContext, useReducer } from 'react';

const createReducerContext = <R extends React.Reducer<any, any>>(
reducer: R,
defaultInitialState: React.ReducerState<R>
) => {
const context = createContext<[React.ReducerState<R>, React.Dispatch<React.ReducerAction<R>>] | undefined>(undefined);
const providerFactory = createFactory(context.Provider);

const ReducerProvider: React.FC<{ initialState?: React.ReducerState<R> }> = ({ children, initialState }) => {
const state = useReducer<R>(reducer, initialState !== undefined ? initialState : defaultInitialState);
return providerFactory({ value: state }, children);
};

const useReducerContext = () => {
const state = useContext(context);
if (state == null) {
throw new Error(`useReducerContext must be used inside a ReducerProvider.`);
}
return state;
};

return [useReducerContext, ReducerProvider, context] as const;
};

export default createReducerContext;
23 changes: 23 additions & 0 deletions src/createStateContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createFactory, createContext, useContext, useState } from 'react';

const createStateContext = <T>(defaultInitialValue: T) => {
const context = createContext<[T, React.Dispatch<React.SetStateAction<T>>] | undefined>(undefined);
const providerFactory = createFactory(context.Provider);

const StateProvider: React.FC<{ initialValue?: T }> = ({ children, initialValue }) => {
const state = useState<T>(initialValue !== undefined ? initialValue : defaultInitialValue);
return providerFactory({ value: state }, children);
};

const useStateContext = () => {
const state = useContext(context);
if (state == null) {
throw new Error(`useStateContext must be used inside a StateProvider.`);
}
return state;
};

return [useStateContext, StateProvider, context] as const;
};

export default createStateContext;
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export { default as createMemo } from './createMemo';
export { default as createReducerContext } from './createReducerContext';
export { default as createReducer } from './createReducer';
export { default as createStateContext } from './createStateContext';
export { default as useAsync } from './useAsync';
export { default as useAsyncFn } from './useAsyncFn';
export { default as useAsyncRetry } from './useAsyncRetry';
Expand Down
66 changes: 66 additions & 0 deletions stories/createReducerContext.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';

import { createReducerContext } from '../src';
import ShowDocs from './util/ShowDocs';

type Action = 'increment' | 'decrement';

const reducer = (state: number, action: Action) => {
switch (action) {
case 'increment':
return state + 1;
case 'decrement':
return state - 1;
default:
throw new Error();
}
};

const [useSharedCounter, SharedCounterProvider] = createReducerContext(reducer, 0);

const ComponentA = () => {
const [count, dispatch] = useSharedCounter();
return (
<p>
Component A &nbsp;
<button type="button" onClick={() => dispatch('decrement')}>
-
</button>
&nbsp;{count}&nbsp;
<button type="button" onClick={() => dispatch('increment')}>
+
</button>
</p>
);
};

const ComponentB = () => {
const [count, dispatch] = useSharedCounter();
return (
<p>
Component B &nbsp;
<button type="button" onClick={() => dispatch('decrement')}>
-
</button>
&nbsp;{count}&nbsp;
<button type="button" onClick={() => dispatch('increment')}>
+
</button>
</p>
);
};

const Demo = () => {
return (
<SharedCounterProvider>
<p>Those two counters share the same value.</p>
<ComponentA />
<ComponentB />
</SharedCounterProvider>
);
};

storiesOf('State|createReducerContext', module)
.add('Docs', () => <ShowDocs md={require('../docs/createReducerContext.md')} />)
.add('Demo', () => <Demo />);
43 changes: 43 additions & 0 deletions stories/createStateContext.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';

import { createStateContext } from '../src';
import ShowDocs from './util/ShowDocs';

const [useSharedText, SharedTextProvider] = createStateContext('');

const ComponentA = () => {
const [text, setText] = useSharedText();
return (
<p>
Component A:
<br />
<input type="text" value={text} onInput={ev => setText(ev.currentTarget.value)} />
</p>
);
};

const ComponentB = () => {
const [text, setText] = useSharedText();
return (
<p>
Component B:
<br />
<input type="text" value={text} onInput={ev => setText(ev.currentTarget.value)} />
</p>
);
};

const Demo = () => {
return (
<SharedTextProvider>
<p>Those two fields share the same value.</p>
<ComponentA />
<ComponentB />
</SharedTextProvider>
);
};

storiesOf('State|createStateContext', module)
.add('Docs', () => <ShowDocs md={require('../docs/createStateContext.md')} />)
.add('Demo', () => <Demo />);
Loading

0 comments on commit 84b8310

Please sign in to comment.