← 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]
}