mediumReact#144

Undo/redo reducer

Prompt

Write historyReducer(state, action) over the shape { past: [], present, future: [] }:

  1. { type: 'SET', value } — pushes current present onto past, sets new present, clears future
  2. { type: 'UNDO' } — moves present to future, pops the last past entry into present
  3. { type: 'REDO' } — inverse of UNDO
  4. UNDO with empty past / REDO with empty future return state unchanged
  5. Pure function — no mutation, no side effects

Solution

function historyReducer(state, action) {
  const { past, present, future } = state
  switch (action.type) {
    case 'SET':
      if (action.value === present) return state
      return { past: [...past, present], present: action.value, future: [] }
    case 'UNDO': {
      if (past.length === 0) return state
      return {
        past: past.slice(0, -1),
        present: past[past.length - 1],
        future: [present, ...future],
      }
    }
    case 'REDO': {
      if (future.length === 0) return state
      const [next, ...rest] = future
      return { past: [...past, present], present: next, future: rest }
    }
    default:
      return state
  }
}
Mentor's take

Undo/redo is the interview's favorite reducer because it proves you understand why reducers must be pure: time travel only works if every state was a new object — mutate the past and there's no past to go back to. This is the concrete payoff of immutability, not the abstract lecture version.

Design notes a mentor would flag:

  • The three-field shape (past / present / future) is the whole design. Undo and redo become array moves. Candidates who store an index into one array get lost in off-by-ones; candidates who store snapshots inside the present conflate data with history.
  • SET clears future — after you undo twice and type something new, redo must die. Branching history (a tree) is the follow-up question; naming that trade-off ("linear history loses the abandoned branch; Photoshop keeps a tree") is a seniority marker.
  • The no-op guards (past.length === 0 → return state) aren't just correctness: returning the same reference means React/Redux bail out of re-rendering. return state and return { ...state } behave identically to your logic and completely differently to React.
  • The unchanged-value guard on SET keeps a same-value dispatch from polluting history with duplicates.

This is also useReducer interview gold: wrap any value in this reducer and you've added undo to a form, a drawing canvas, a filter panel — with zero library code.

Say it: "History is past/present/future arrays and every transition builds new objects — which is exactly why reducers must be pure: undo is only possible if the past was never mutated."