Skip to content
React logic · Senior · about 25 minutes

Undo and redo

Implement undoable(reducer). It returns a reducer over { past, present, future }. { type: 'undo' } moves present to the front of future and the last past state to present. { type: 'redo' } does the reverse. Any other action goes to the inner reducer: when it returns a new present, push the old one onto past and clear future; when it returns the same present, return the same state. With nothing to undo or redo, return the same state. Called with an undefined state, start with empty past and future and the inner reducer's own initial state. In the tests the inner reducer is a counter starting at 0 (inc, and add with by); same tells whether the last action returned the same reference.

Start from

function undoable(reducer) {
  // Return a reducer over { past, present, future }.
}

Hints

Each hint gives away a little more. Open one only when you need it.

  1. Hint 1

    Build the initial state once: call the inner reducer with undefined and any action to get its initial present.

  2. Hint 2

    undo and redo are each one object literal built with slice and spread. Check the empty case first.

  3. Hint 3

    For other actions, compare the new present with the old one by reference before building a new state.

Solution

From here on the page gives the method away. If you are still solving, go back to the hints.

JavaScript
function undoable(reducer) {
  const initial = {
    past: [],
    present: reducer(undefined, { type: "@@init" }),
    future: [],
  };
  return function (state = initial, action) {
    const { past, present, future } = state;
    if (action.type === "undo") {
      if (!past.length) return state;
      return {
        past: past.slice(0, -1),
        present: past[past.length - 1],
        future: [present, ...future],
      };
    }
    if (action.type === "redo") {
      if (!future.length) return state;
      return {
        past: [...past, present],
        present: future[0],
        future: future.slice(1),
      };
    }
    const next = reducer(present, action);
    if (next === present) return state;
    return { past: [...past, present], present: next, future: [] };
  };
}

What an interviewer looks for

A higher-order reducer, and a common senior exercise for editors and forms. The scoring is in the rule that a new action clears the redo history.