30% offEnding soon
useImmerReducerLoading saved progress…

useImmerReducer

useImmerReducer(recipe, initialState) is a useReducer whose reducer mutates a draft instead of building the next state with spreads. You write draft.user.name = 'Ada' or draft.items.push(item), and the hook produces the immutable next state a reducer is required to return — with the branches you did not touch shared by reference. It is the hook behind use-immer and the shape of every Redux Toolkit createSlice.

You are handed a working produce(base, recipe) — it is the immer-produce question, complete. Your job is only the wiring: turn it into a reducer hook. The interesting part is what changes once produce is your reducer, so the exercise stays single-axis.

Signature

function useImmerReducer<S, A>(
  // Mutate `draft` in place, OR return a replacement state — never both.
  recipe: (draft: S, action: A) => void | S,
  initialState: S,
): [state: S, dispatch: (action: A) => void];

Examples

const recipe = (draft, action) => {
  if (action.type === 'increment') draft.count += 1;
  if (action.type === 'setBy') draft.count += action.by;
};

const [state, dispatch] = useImmerReducer(recipe, { count: 0 });
dispatch({ type: 'increment' });     // state.count is 1 on the next render
dispatch({ type: 'setBy', by: 5 });  // 6
const recipe = (draft, action) => {
  if (action.type === 'add') draft.items.push({ id: action.id, done: false });
  if (action.type === 'reset') return action.fresh; // return a replacement
};

const [board, dispatch] = useImmerReducer(recipe, {
  title: 'todo',
  items: [],
  settings: { theme: 'dark' },
});

dispatch({ type: 'add', id: 1 });
// board.items grows; board.settings is the SAME object as before (shared).

Notes

  • The produce is provided — wire it into a useReducer; do not rebuild it. See immer-produce for how it works.
  • Mutate the draft and return nothing — that is the normal case. Returning a value replaces the state wholesale (a reset); mutating and returning throws.
  • The next state is frozen — a later state.items.sort() or state.list.push(x) downstream throws instead of silently corrupting the store.
  • Untouched slices keep their reference — structural sharing, so a React.memo child reading an unchanged slice skips its re-render.
  • dispatch is stable — its identity never changes, so it is safe to pass to memoized children or capture in a mount-only effect.
  • Plain objects and arrays only — state need not hold Maps, Sets, Dates, or class instances.