Undo and redo with useReducer means keeping three values: the present state plus two history arrays. Every new action pushes the old present into the past and clears the future, while undo and redo move between the stacks.
Because reducers are pure, the whole history is just data you can inspect and test. The result feels like a text editor's undo stack, but it works for any state.
Shape the history
The state has a past array, a present value, and a future array. A set action records the current present before replacing it, so nothing is lost. The three fields always satisfy one invariant: present is on screen, past holds the steps before it, and future holds the steps after it.
const initial = { past: [], present: 0, future: [] };
function reducer(state, action) {
switch (action.type) {
case "set":
return { past: [...state.past, state.present], present: action.value, future: [] };
default:
return state;
}
}The set case appends the old present to past and empties future. Emptying future is what makes redo unavailable after a new change, because the user branched away from the path they could replay.
Move through history with undo and redo
Undo pops the last value from past into present and pushes the old present onto future. Redo does the reverse, so the two directions reuse the same array moves, just mirrored.
case "undo":
return {
past: state.past.slice(0, -1),
present: state.past[state.past.length - 1],
future: [state.present, ...state.future]
};
case "redo":
return {
past: [...state.past, state.present],
present: state.future[0],
future: state.future.slice(1)
};Undo reads the last item of past as the new present. Redo reads the first item of future. Both keep the untouched side of history intact.
When past is empty, undo has nothing to pop, and when future is empty, redo has nothing to replay.
Cap the history so it stays small
Unbounded history grows with every change. In the set case, keep only the most recent steps by slicing the past array, such as state.past.slice(-50). Older steps fall off, which bounds memory while keeping undo and redo fast.
Redo already stays bounded because any new set clears the future. Bounding the past this way keeps the cost of each action constant no matter how long the user edits.
Wire the buttons
The component renders the present value and two buttons that dispatch undo and redo. Disable each button when its history is empty, and keep them as real buttons so keyboard users can trigger them.
function Counter() {
const [state, dispatch] = useReducer(reducer, initial);
return (
<div>
<button disabled={state.past.length === 0} onClick={() => dispatch({ type: "undo" })}>Undo</button>
<span>{state.present}</span>
<button disabled={state.future.length === 0} onClick={() => dispatch({ type: "redo" })}>Redo</button>
</div>
);
}With no past steps, Undo is disabled. After one undo, Redo becomes available because future now holds the reverted value. As the user clicks, the number walks backward and forward while the disabled state of each button updates.
See how to write actions and reducers for the naming rules behind these cases, or update arrays in React state for the immutable array operations.
Rune AI
Key Insights
- Keep past, present, and future in one state object.
- Every set pushes the old present into past and clears future.
- Undo moves present into future and pops past.
- Redo does the reverse.
- Disable buttons when their history is empty.
Frequently Asked Questions
Why keep past and future as arrays?
What clears the future history?
How do I cap the history length?
Conclusion
Undo and redo with useReducer means keeping present state plus past and future arrays. Each set pushes the old present into past and clears future, while undo and redo move values between the stacks and update the present.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.