Skip to content

Instantly share code, notes, and snippets.

@swyxio
Last active May 4, 2023 02:15
Show Gist options
  • Select an option

  • Save swyxio/f18fe6dd4c43fddb3a4971e80114a052 to your computer and use it in GitHub Desktop.

Select an option

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
// 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>
)
}
@apo1798
Copy link
Copy Markdown

apo1798 commented Jan 2, 2023

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 ^_^

@swyxio
Copy link
Copy Markdown
Author

swyxio commented Jan 12, 2023

thanks! updating it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment