Skip to content

Instantly share code, notes, and snippets.

@LongLiveCHIEF
Forked from swyxio/createCtx-noNullCheck.tsx
Created January 31, 2022 16:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save LongLiveCHIEF/f46fc11f56847e6e3011fc244a6af590 to your computer and use it in GitHub Desktop.
Save LongLiveCHIEF/f46fc11f56847e6e3011fc244a6af590 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>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment