Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save joeyjiron06/6c167d69f58b41d41b22175e05538b43 to your computer and use it in GitHub Desktop.
Save joeyjiron06/6c167d69f58b41d41b22175e05538b43 to your computer and use it in GitHub Desktop.

React Context + Reducer + Typescript

I followed the recommendation provided by the react docs that suggest how to use a reducer in conjuction with context.

There is a lot of boiler plate just to add 1 action and a reducer that handles the action.

Before

This is what it looks like before my simplified API, which at first glance doesn't look bad for small demo apps, but quickly adds up over time.

/* eslint-disable @typescript-eslint/no-explicit-any */
import { Context, Dispatch, createContext, useReducer } from 'react'

interface ArtistsState {
  artists: { id: number; name: string }[]
  activeArtistId?: number
}

const initialState: ArtistsState = {
  artists: [{ id: 1, name: 'The Betails' }],
  activeArtistId: undefined,
}
const ArtistsContext = createContext([initialState, () => {}])

// ACTIONS
function artistClicked(id: number) {
  return <const>{
    type: 'artistClicked',
    payload: id,
  }
}

type Action = ReturnType<typeof artistClicked>

function artistsReducer(state: typeof initialState, action: Action) {
  switch (action.type) {
    case 'artistClicked':
      return {
        ...state,
        activeArtistId: action.payload,
      }
    default:
      return state
  }
}


// in app component

function App() {
  const [state, dispatch] = useReducer(artistsReducer) 

  return <ArtistsContext.Provider value={[state, dispatch]}>
      <!--   other stuff here  -->
    </ArtistsContext.Provider>
}

// in child component

function MyComponent() {
  const [state, dispatch] = useContext(ArtistsContext)
  
  const onClick = useCallback(() => {
    dispatch(artistClicked( someIDHere ))
  }, [dispatch])
  
  return (
    <button onClick={onClick} >click me!</button>
  )
}

After

To reduce that boilerplate, I came up with a simplified API that you can use like this

import { createStore } from './reducerContext.tsx'



export const artistsStore = createStore({
  // initial state goes here
   artists: [ {id: 1, name: "The Betails"} ],
   activeArtistId: null,
}, {
  // actions go here. they should return a reducer function
  artistClicked(artistId: string) {
    return state => ({
      ...state,
      activeArtistId: artistId
    })
  }

});

// add this to your app at the highest level possible artistsStore.Provider
// then in a child component you can do this:

import { actionStore } from './actionStore';
const {  artistClicked } = actionStore.actions;

function MyComponent() {
  const [state, dispatch] = useContext(artistsStore.Context)
  
  const onClick = useCallback(() => {
    dispatch(artistClicked( someIDHere ))
  }, [dispatch])
  
  return (
    <button onClick={onClick} >click me!</button>
  )
}
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Context, Dispatch, createContext, useReducer } from 'react'
/**
* An action that will be dispatched
*/
type Action<K, V> = {
type: K
payload: V
}
/**
* An object where the keys are the action type and the values
* are functions that return a reducer function. These are
* the actions that you author as the developer and internally
* you'll get back an action with type (that comes from the key)
* and a payload which is the arguments to that function.
*/
interface Actions<T> {
[key: string]: (...args: any[]) => (state: T) => T
}
/**
* You pass in an actions function as described above and you will
* get back a new object where the keys are the same as the ones you
* pass in action and the values will be a function that returns an
* action where the type on the action is the name of the key and
* the payload is the params that are passed to the function.
*/
type ActionFactories<A extends Actions<any>> = {
[K in keyof A]: (...args: Parameters<A[K]>) => Action<K, Parameters<A[K]>>
}
/**
* Create a store that has Context, Provider, and a reducer
* Helps reduce boilerplate so you don't have to do this in every project.
* To get types to work properly have them infered, meaning when you call
* this function, don't pass in the types and let typescript do it's thing!
*/
export function createStore<T, A extends Actions<any extends T ? T : never>>(
initialState: T,
actions: A
): {
actions: ActionFactories<A>
Provider: ({ children }: { children: React.ReactNode }) => JSX.Element
Context: Context<[T, Dispatch<Action<keyof A, Parameters<A[keyof A]>>>]>
} {
const StoreContext = createContext<
[T, Dispatch<Action<keyof A, Parameters<A[keyof A]>>>]
>([initialState, () => {}])
function reducer(state: T, action: Action<any, any>) {
const actionReducer = actions[action.type]
return actionReducer(...action.payload)(state)
}
function Provider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<StoreContext.Provider value={[state, dispatch]}>
{children}
</StoreContext.Provider>
)
}
return {
Provider,
Context: StoreContext,
actions: actions as unknown as ActionFactories<A>,
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment