useReducer in TypeScript


Here's how to type React.useReducer in TypeScript.

import React from 'react'

/* 1. Create a union type for the actions that you have  */
type Action = SearchAction | ToggleUserAction

interface User { 
  id: string
}

/* 2. You can differentiate actions based on type string */
interface SearchAction {
  type: 'search'
  term: string
}

interface ToggleUserAction {
  type: 'toggle'
  id: string
}

/* 3. Define your state with all the bits that need to relate to each other */
interface State {
  term: string | undefined
  users: User[]
  filteredIds: string[]
  selectedIds: string[]
}

/* 4. Define your reducer using the interfaces you've just declared */
function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'search':
      return {
        ...state,
        term: action.term,
        filteredIds: filterIds(state.users, action.term), // offscreen
      }
    case 'toggle':
      return {
        ...state,
        selectedIds:
          action.id === 'all'
            ? state.selectedIds.length === state.users.length
              ? []
              : state.users.map((user) => user.id)
            : state.selectedIds.includes(action.id)
              ? state.selectedIds.filter((id) => id !== action.id)
              : [...state.selectedIds, action.id],
      }
    default:
      return state
  }
}

function UserCombobox(props: {
  label: string
  name: string
  users: User[]
  defaultValue: string[]
}) {
  /* 5. The reducer requires the action, even if just this union type, to be an array   */
  /* 6. Feed it initial state, matching the State interface */
  const [state, dispatch] = React.useReducer<State, [Action]>(reducer, {
    term: '',
    users: props.users,
    filteredIds: [],
    selectedIds: props.defaultValue ?? [],
  })

  /* 7. One of the event handlers and call to dispatch */
  function handleSearch(evt: React.ChangeEvent<HTMLInputElement>) {
    const term = evt.target.value ?? ''
    dispatch({ type: 'search', term })
  }

  /* 8. Some use of current state from the reducer */
  return (
    <input
      type="search"
      name="term"
      onChange={handleSearch}
      value={state.term}
    />
  )
}

How do you like them apples?