Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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>
)
}
@EthanSK

This comment has been minimized.

Copy link

EthanSK commented Aug 16, 2019

createCtx-useReducer.tsx is useful! Thanks.
in line 1, why do state and action have the same type? if we wanted action to have a different type, it would need a different generic?

@sw-yx

This comment has been minimized.

Copy link
Owner Author

sw-yx commented Aug 16, 2019

you're right. i was just prototyping it and thats what i put but you're right, in fact i just ran into this a couple days ago and just didnt fix my gist. wanna take a crack at doing it right? and i'll fix my gist

@EthanSK

This comment has been minimized.

Copy link

EthanSK commented Aug 16, 2019

I wish i were good enough at typescript to :( Was kinda relying on this to help me :p

@sw-yx

This comment has been minimized.

Copy link
Owner Author

sw-yx commented Aug 16, 2019

try.

@stevengpwc

This comment has been minimized.

Copy link

stevengpwc commented Aug 24, 2019

Hi, this is really useful.
What if I have multiple contexts? Some of them are from createCtx-useState and some are from createCtx-useReducer? Can I put them all into one big provider?

@sw-yx

This comment has been minimized.

Copy link
Owner Author

sw-yx commented Aug 26, 2019

@stevengpwc i'm not sure i understand. can you give some examples? maybe just put up a small repo

@sw-yx

This comment has been minimized.

Copy link
Owner Author

sw-yx commented Aug 26, 2019

@EthanSK i have updated

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>
  )
}
@pandaCure

This comment has been minimized.

Copy link

pandaCure commented Jan 7, 2020

how to use useReducer the three argument by ts

@odGit

This comment has been minimized.

Copy link

odGit commented Mar 19, 2020

@sw-yx how about changing createCtx into following:

type StoreApi = {
  state: typeof initialState;
  dispatch: React.Dispatch<Action>;
}

const ctx = React.createContext<StoreApi>({
  state: initialState,
  dispatch: () => {}, //because it inherits it's type from StoreApi 
})

function Provider(props: React.PropsWithChildren<{}>) {
// changed <React.Reducer<StateType, ActionType>> to <React.Reducer<AppState, Action>>
  const [state, dispatch] = React.useReducer<React.Reducer<AppState, Action>>(reducer, initialState)
  return <ctx.Provider value={{ state, dispatch }} {...props} />
}

function useCtx() {
  const context = React.useContext(ctx)
  if (context === undefined) throw new Error(`No provider for AppContext given`)
  return context
}

export {Provider, useCtx}

// example using useCtx
const {state, dispatch} = useCtx();

Are there any downsides to this approach ?

@sw-yx

This comment has been minimized.

Copy link
Owner Author

sw-yx commented Mar 25, 2020

idk, if it works for you then good for you! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.