-
-
Save swyxio/f18fe6dd4c43fddb3a4971e80114a052 to your computer and use it in GitHub Desktop.
// create context with no upfront defaultValue | |
// without having to do undefined check all the time | |
function createCtx<A>() { | |
const ctx = React.createContext<A | undefined>(undefined) | |
function useCtx() { | |
const c = React.useContext(ctx) | |
if (!c) throw new Error("useCtx must be inside a Provider with a value") | |
return c | |
} | |
return [useCtx, ctx.Provider] as const | |
} | |
// usage - no need to specify value upfront! | |
export const [useCtx, SettingProvider] = createCtx<string>() | |
export function App() { | |
// get a value from a hook, must be in a component | |
const key = useLocalStorage('key') | |
return ( | |
<SettingProvider value={key}> | |
<Component /> | |
</SettingProvider> | |
) | |
} | |
export function Component() { | |
const key = useCtx() // can still use without null check! | |
return <div>{key}</div> | |
} | |
function useLocalStorage(a: string) { | |
return 'secretKey' + a | |
} |
export function createCtx<StateType, ActionType>( | |
reducer: React.Reducer<StateType, ActionType>, | |
initialState: StateType, | |
) { | |
const defaultDispatch: React.Dispatch<ActionType> = () => initialState // we never actually use this | |
const ctx = React.createContext({ | |
state: initialState, | |
dispatch: defaultDispatch, // just to mock out the dispatch type and make it not optioanl | |
}) | |
function Provider(props: React.PropsWithChildren<{}>) { | |
const [state, dispatch] = React.useReducer<React.Reducer<StateType, ActionType>>(reducer, initialState) | |
return <ctx.Provider value={{ state, dispatch }} {...props} /> | |
} | |
return [ctx, Provider] as const | |
} | |
// usage | |
const initialState = { count: 0 } | |
type AppState = typeof initialState | |
type Action = | |
| { type: 'increment' } | |
| { type: 'add'; payload: number } | |
| { type: 'minus'; payload: number } | |
| { type: 'decrement' } | |
function reducer(state: AppState, action: Action): AppState { | |
switch (action.type) { | |
case 'increment': | |
return { count: state.count + 1 } | |
case 'decrement': | |
return { count: state.count - 1 } | |
case 'add': | |
return { count: state.count + action.payload } | |
case 'minus': | |
return { count: state.count - action.payload } | |
default: | |
throw new Error() | |
} | |
} | |
const [ctx, CountProvider] = createCtx(reducer, initialState) | |
export const CountContext = ctx | |
// top level example usage | |
export function App() { | |
return ( | |
<CountProvider> | |
<Counter /> | |
</CountProvider> | |
) | |
} | |
// example usage inside a component | |
function Counter() { | |
const { state, dispatch } = React.useContext(CountContext) | |
return ( | |
<div> | |
Count: {state.count} | |
<button onClick={() => dispatch({ type: 'increment' })}>+</button> | |
<button onClick={() => dispatch({ type: 'add', payload: 5 })}>+5</button> | |
<button onClick={() => dispatch({ type: 'decrement' })}>-</button> | |
<button onClick={() => dispatch({ type: 'minus', payload: 5 })}>-5</button> | |
</div> | |
) | |
} |
export function createCtx<A>(defaultValue: A) { | |
type UpdateType = React.Dispatch<React.SetStateAction<typeof defaultValue>> | |
const defaultUpdate: UpdateType = () => defaultValue | |
const ctx = React.createContext({ state: defaultValue, update: defaultUpdate }) | |
function Provider(props: React.PropsWithChildren<{}>) { | |
const [state, update] = React.useState(defaultValue) | |
return <ctx.Provider value={{ state, update }} {...props} /> | |
} | |
return [ctx, Provider] as const | |
} | |
// usage | |
const [ctx, TextProvider] = createCtx("someText") | |
export const TextContext = ctx | |
export function App() { | |
return ( | |
<TextProvider> | |
<Component /> | |
</TextProvider> | |
) | |
} | |
export function Component() { | |
const { state, update } = React.useContext(ctx) | |
return ( | |
<label> | |
{state} | |
<input type="text" onChange={e => update(e.target.value)} /> | |
</label> | |
) | |
} |
thanks!!! tweeted it out!
Hi, I use 'createContext-useReducer', this gist has been very helpful. But I have an issue that eslint does not accept the dispatch
function as stable and litters the screen with warnings. Considering your previous example, to explain the issue I extend your example,
// ... copy of createCtx-useReducer gist
// example usage inside a component
function Counter() {
const { state, dispatch } = React.useContext(CountContext);
useEffect(() => {
dispatch({ type: 'operation-to-calculate-after-render' }); // React Hook useEffect has missing dependencies: 'dispatch'
}, []);
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'add', payload: 5 })}>+5</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'minus', payload: 5 })}>+5</button>
</div>
)
}
How to convince eslint that the dispatch
does not need to be in the dependencies? If we add dispatch
to the dependencies then useEfffect
goes to infinite loop.
The dispatch
function originates from the useReducer
, however, since it is retrieved via the useContext
, from the Provider
, it is not treated correctly in the useEffect
. I see no solution provided by ReactJS for this situation and that they left developers with an incomplete system.
Your example above reduced to a brief complete example below,
const initState = { a: 1 };
type Action = { type: 'inc' };
const defaultDispatch: React.Dispatch<Action> = () => initState;
const ctx = React.createContext({
state: initState,
dispatch: defaultDispatch
});
function reducer(action, state)
{
if (!action) return;
return state;
}
function App(props: React.PropsWithChildren)
{
const { state, dispatch } = React.useReducer(reducer, initState);
return (
<ctx.Provider value={{ state, dispatch }} {...props}>
<Component />
</ctx.Provider>
);
}
function Component()
{
const { state, dispatch } = React.useContext(ctx);
React.useEffect(() => {
dispatch({ type: 'inc' }); // React Hook useEffect has missing dependencies: 'dispatch'
}, []);
return <div>{state.a}</div>;
}
The sample may not look useful, why calling dispatch in useEffect, more complex sample can be:
React.useEffect(() => {
if (!aState) return;
// Call dispatch only after a certain complex state has been reach by the app user.
dispatch({ type: 'inc' }); // React Hook useEffect has missing dependencies: 'dispatch'
}, [aState]);
I had unnecessary re-renders because of plain object passed into the provider value
. But wrapping the value
in a useMemo
hook solved our problem. We found out, that when we had a state
change in the provider, it re-rendered. So the value
object was new on each render. Which caused a change in all of the consumers, which caused re-renders.
Maybe it could be the same issue? If not, it will definitely solve at least some of unnecessary re-renders that you could have.
function App(props: React.PropsWithChildren) {
const { state, dispatch } = React.useReducer(reducer, initState);
const providerValue = useMemo(() => ({ state, dispatch }), [state, dispatch])
return (
<ctx.Provider value={providerValue} {...props}>
<Component />
</ctx.Provider>
);
}
Hi sw-yx, thanks for sharing this three sample useful code snippet for useContext in TypeScript. Just found a tiny typo in the reducer code in line 60
<button onClick={() => dispatch({ type: 'minus', payload: 5 })}>+5</button> // should be `-5`
Wishing you a great day and happy New Year ^_^
thanks! updating it
Based on these concept i developed a npm package
mini-state
You can use this by installing
npm install mini-state
and use this like below,Initialize global state
Adding provider in root of our app
Usage in app with a counter example
Congratulations 🎉 We successfully added this to your application
Live Example on Codesandbox