Last active
May 4, 2023 02:15
-
-
Save swyxio/f18fe6dd4c43fddb3a4971e80114a052 to your computer and use it in GitHub Desktop.
better createContext APIs with setters, and no default values, in Typescript. this is documented in https://github.com/typescript-cheatsheets/react-typescript-cheatsheet/blob/master/README.md#context
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
) | |
} |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I had unnecessary re-renders because of plain object passed into the provider
value
. But wrapping thevalue
in auseMemo
hook solved our problem. We found out, that when we had astate
change in the provider, it re-rendered. So thevalue
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.