← from Blog

React Reducer Patterns

December 28, 2022 2 min read

The reducer pattern is one of the commonly used patterns in React. Here are some patterns that can be useful for you when using it.

Toogle Boolean

The hooks is usefull for controlling toggle or other elements with a boolean state.

interface ToggleProps {
  initialState?: boolean;
}

function useToggle({ initialState = false }: ToogleProps) {
  const [enabled, setEnabled] = React.useReducer((previous) => !previous, initialState)

  return [enabled, setEnabled]
}

Select or deselect an item

interface SelectionProps<T> {
  isEqual?: (prev: T | undefined, next: T) => boolean;
  initialState?: T | undefined;
}

function useSelection<T>({ isEqual = (prev, next) => prev === next, initialState = undefined }: SelectionProps = {}) {
  const [selection, setSelection] = React.useReducer(
    (prev: T | undefined, next: T) =>
      (isEqual(prev, next) ? undefined : next),
    initialState,
  )

  return [select, setSelection]
}

Selecting multiple items

interface MultiSelectionProps<T> {
  isEqual?: (prev: T, next: T) => boolean;
  initialState?: T[];
}

function useMultipleSelection<T>({
  isEqual = (prev, next) => prev === next,
  initialState = [],
}: MultipleSelectionProps) {
  const [selection, setSelection] = React.useReducer(
    (prev: T[], next: T[]) => {
      const index = prev.findIndex((x) => isEqual(x, next))
      return index === -1 ? [...prev, next] : prev.filter((_, i) => i !== index)
  }, initialState)

  return [selection, setSelection]
}
type Menu = undefined | 'file' | 'edit' | 'view' // undefined mean closed

const [openMenu, tap] = React.useReducer(
  (current: Menu, action: Menu) => {
    return action === current ? null : action
}, undefined)

Jitter Generator

Inspired by AWS: Exponential Backoff And Jitter

const baseMs = 5
const capMs = 2000
const [{ attempt, delayMs }, dispatch] = React.useReducer(
  (state, random) => {
    return {
      attempt: state.attempt + 1,
      delayMs: random * Math.min(capMs, baseMs * Math.pow(2, attempt)),
    }
  },
  { attempt: 0, delay: baseMs },
)

const nextAttempt = React.useCallback(() => dispatch(Math.random()), [dispatch])

Logical Clock or Force Update

const [t, tick] = React.useReducer((n) => n + 1, 0)

function useForceUpdate() {
  const [ignored, forceUpdate] = React.useReducer((n) => n + 1, 0)
  return [ignored, forceUpdate]
}