Undo and redo with two stacks
Keep past states on an undo stack and undone states on a redo stack, so undo and redo each move one state between them.
O(1) per operationSpace O(h)3 practice problemsStep-through pictureThe idea
Every editor needs undo and redo. The simplest correct design keeps three things: present, the current state; undoStack, the states before it, most recent on top; and redoStack, the states you undid, most recently undone on top.
A new change pushes present onto undoStack, makes the new state the present, and empties redoStack, because the undone future no longer fits after a new change. Undo pushes present onto redoStack and pops the top of undoStack into present. Redo does the mirror image.
A stack is the right tool because undo always goes back to the most recent state first: last in, first out. Each operation is O(1).
An everyday example
Two piles of drawings
You draw on a sheet. Before each change, you put a copy of the current drawing on the left pile. To undo, you put your current sheet on the right pile and take the top sheet from the left pile. To redo, you do the opposite. If you draw something new after undoing, the sheets on the right pile describe a future that didn't happen, so you throw that pile away.
| In the example | In the code |
|---|---|
| The sheet in front of you | this.present |
| The left pile of earlier drawings | this.undoStack |
| The right pile of undone drawings | this.redoStack |
| Drawing something new | commit(next), which also empties redoStack |
| Swapping a sheet between piles | undo() and redo() |
From the slow way to undo and redo with two stacks
- Start with the obvious way: keep one
previousvalue next to the current one, and swap them on undo. That gives one level of undo. Press undo twice and you are back where you started, because the swap just toggles between two states. - To undo many steps, keep every past state. The state you go back to first is always the most recent one, then the one before it. Most recent first is last in, first out, so the past states belong on a stack: push the current state before each change, pop it on undo.
- But undo then throws away the state you were on, and redo needs it. So when undoing, push the current state onto a second stack instead of dropping it. Redo is the mirror image: push the current state back onto the undo stack and pop from the redo stack.
- One rule is left. If the user undoes and then makes a new change, the states on the redo stack belong to a future that will never happen. Mixing them in would jump between two timelines, so a new change empties the redo stack. Those two stacks around a
presentvalue are undo and redo history.
How it works
- Start with
presentset to the initial state and two empty stacks. commit(next): pushpresentontoundoStack, setpresent = next, and clearredoStack.undo(): ifundoStackis empty, there's nothing to undo, so returnpresent. Otherwise pushpresentontoredoStackand popundoStackintopresent.redo(): ifredoStackis empty, returnpresent. Otherwise pushpresentontoundoStackand popredoStackintopresent.- Each method returns the new
present, so the caller can render it.
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
class History {constructor(initial) {this.present = initial;this.undoStack = [];this.redoStack = [];}commit(next) {this.undoStack.push(this.present);this.present = next;this.redoStack = [];}undo() {if (this.undoStack.length === 0) return this.present;this.redoStack.push(this.present);this.present = this.undoStack.pop();return this.present;}redo() {if (this.redoStack.length === 0) return this.present;this.undoStack.push(this.present);this.present = this.redoStack.pop();return this.present;}}
How fast it is
| Time | O(1) per operation |
|---|---|
| Extra memory | O(h) |
Each method does at most one push and one pop. The stacks hold one entry per saved state, h in total.
- Best case
O(1)Any call:
undoandredodo one push and one pop, andcommitdoes one push and one assignment. - Average case
O(1)The same for every call, however long the history is.
- Worst case
O(1)Still one push and one pop. Replacing
redoStackwith[]is one assignment; the old array is cleaned up later by the engine. Making thenextstate is the caller's job and can cost more.
Common mistakes
- Forgetting to clear
redoStackon a new change. Redo then jumps to a state from an abandoned timeline. - Saving a reference to a mutable object and then changing it. Every history entry points at the same object and undo shows the latest version. Save copies or use immutable updates.
- Pushing a history entry for every keystroke. Undo then removes one letter at a time. Group quick changes into one entry.
- Letting history grow forever in a long session. Cap it and drop the oldest entries.
When to use it
Look for these clues in a problem:
- The question involves undo, redo, back and forward, or history of states.
- Commands arrive one by one and some of them reverse earlier ones.
- After going back, a new action should discard the forward history.
- You need the most recent item first, which is last in, first out.
Check yourself
Answer each one in your head first, then open it.
After commit("A"), commit("B"), undo(), commit("C"), what does redo() do?
Nothing: it returns "C". commit("C") emptied the redo stack, so "B" can't come back.
Why is a stack the right structure for past states?
Undo always goes back to the most recent state first, then the one before it. That is last in, first out, which is exactly what a stack gives.
After 3 commits and then 3 undos, how big are the two stacks?
undoStack is empty and redoStack holds 3 states. Each undo moved one state from one stack to the other.
Why does changing an object in place break the history?
The stacks store references. If every entry points to the same object, changing it changes every entry, so undo shows the newest content.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 3 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingGo further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
Think of the history as one line of states, oldest to newest, with a cursor on the one the user sees. The class keeps this rule: undoStack from bottom to top, then present, then redoStack from top to bottom, spells out that whole line in order. The states before the cursor are on the undo stack, nearest on top. The states after it are on the redo stack, nearest on top.
Each method keeps the rule. undo moves the cursor one step back: the nearest earlier state comes off the top of undoStack, and the old present becomes the nearest later state, on top of redoStack. redo is the same move forward. commit cuts the line right after the cursor and adds the new state: the old present joins the earlier states, and the later states are dropped by emptying redoStack.
The empty-stack checks handle the two ends of the line: with nothing before the cursor, undo can't move, and with nothing after it, redo can't. Because undo and redo are exact opposites, undoing j times and then redoing j times always returns to the same state, as long as no commit happened in between.
Going deeper
3 more ideas
Why the redo stack is cleared on a new change
Say you type a, b, c, undo twice to get a, then type x. The states ab and abc came after a in an old timeline. Redoing to ab after ax would mix two timelines. Clearing redoStack keeps one straight line of history. Some apps keep the old branches in an undo tree instead, but most editors do the simple thing.
States or commands
This version stores whole states, which is simple and works well with immutable data, like Redux state. Large documents often store commands instead: each entry knows how to apply and revert a small change, such as "insert x at position 5". Editors also merge quick typing into one entry, so one undo removes a word instead of a single letter.
How fast it is
Each operation is a push and a pop, so O(1). Memory grows with the number of saved states, O(h) for h history entries. Many apps cap the history, dropping the oldest entries, to bound memory.
Work it by hand
Every step in a table, with why
Edit a greeting, undo past the start, redo, and then make a new change that wipes out the redo stack.
new History("Hi"), then commit, commit, undo, undo, undo, redo, commit, redo, undo
| Step | Call | undoStack | present | redoStack | Why |
|---|---|---|---|---|---|
| 1 | new History("Hi") | [] | "Hi" | [] | Nothing to undo or redo yet. |
| 2 | commit("Hi Sam") | ["Hi"] | "Hi Sam" | [] | The old present is saved on the undo stack before moving on. |
| 3 | commit("Hi Sam!") | ["Hi", "Hi Sam"] | "Hi Sam!" | [] | The newest saved state is on top, at the right. |
| 4 | undo() | ["Hi"] | "Hi Sam" | ["Hi Sam!"] | "Hi Sam!" goes to the redo stack and "Hi Sam" comes back. |
| 5 | undo() | [] | "Hi" | ["Hi Sam!", "Hi Sam"] | Back to the start. The most recently undone state, "Hi Sam", is on top of the redo stack. |
| 6 | undo() | [] | "Hi" | ["Hi Sam!", "Hi Sam"] | The undo stack is empty, so it returns present and changes nothing. |
| 7 | redo() | ["Hi"] | "Hi Sam" | ["Hi Sam!"] | Redo pops "Hi Sam", the top of the redo stack, and saves "Hi" for undo. |
| 8 | commit("Hi Max") | ["Hi", "Hi Sam"] | "Hi Max" | [] | A new change. "Hi Sam!" belonged to the old future, so the redo stack is emptied. |
| 9 | redo() | ["Hi", "Hi Sam"] | "Hi Max" | [] | Nothing to redo, so nothing changes. |
| 10 | undo() | ["Hi"] | "Hi Sam" | ["Hi Max"] | Undo still works on the new timeline: "Hi Max" can be redone. |
The final present is "Hi Sam". "Hi Sam!" can never come back, because commit("Hi Max") started a new timeline after it was undone.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Undo and redo with two stacks | O(1) | O(h) | Most editors and forms: simple, O(1) per action, and easy to prove correct. |
| One array and an index | O(1) | O(h) | You also want to jump to any point, like a history panel in a design tool. |
| Commands with apply and revert | O(1) | O(h) | States are large, like long documents, so storing small changes saves a lot of memory per entry. |
| Undo tree | O(1) | O(h) | Users must never lose work by undoing and then typing, as in Vim. |
Extra memory: O(h) entries for h saved states. If each state is a full copy of a big document, memory is h times its size; immutable updates that share unchanged parts keep it small.
How the steps grow
- Undo and redo with two stacks, One array and an index, Commands with apply and revert, Undo tree
O(1)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Undo and redo with two stacks | 1 | 1 | 1 |
| One array and an index | 1 | 1 | 1 |
| Commands with apply and revert | 1 | 1 | 1 |
| Undo tree | 1 | 1 | 1 |
Inputs that need care
6 tricky inputs
undo()right afternew History("Hi")undoStackis empty, so it returns"Hi"and both stacks stay empty.redo()right after acommitcommitemptiedredoStack, so it returnspresentunchanged.commit("Hi")whilepresentis already"Hi"- The code saves a duplicate, so the next undo seems to do nothing. Skip a commit when the new state equals
present. - Committing the same object after changing it in place
- Every entry points to the same object, so undo shows the latest version. Commit a new object for each change, or a copy.
- A session with 50,000 edits
- The undo stack grows forever. Cap it and drop the oldest entry when it's full;
shifton an array isO(h), so a ring buffer keeps itO(1). - Undo in a text editor where the cursor moved
- Restoring only the text puts the cursor in the wrong place. Save the selection in each entry, so undo also puts the cursor back where the change was made.
Variations and follow-ups
Twists interviewers add
Store changes instead of whole states
Each entry is a command that knows how to apply and revert one small change, like "insert x at 5", or a pair of patches. Immer's produceWithPatches returns both the patches and the inverse patches. Big documents then cost memory per change, not per copy.
Grouping quick changes
Typing a word letter by letter shouldn't need one undo per letter. Editors merge changes made close together in time into one entry. ProseMirror's history does this with its newGroupDelay option.
An undo tree
Instead of dropping the redo states on a new change, keep them as a branch. Vim does this: :undolist shows the branches, and g- and g+ move through every state in time order, even ones on abandoned branches.
One array and an index
Keep all states in one array and an index for the present. Undo and redo move the index, and a new change cuts the array after it. This is how browser back and forward behave, and it makes a history panel that jumps to any step easy.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from "undo", "redo", "back and forward", or a list of operations where some reverse earlier ones.
- Explain the timeline picture: past states on one stack, future states on the other,
presentin the middle. Say why a new change empties the redo stack. - Test by hand: undo on an empty history, undo then commit then redo, and redo all the way after undoing everything.
- Expect follow-ups: "cap the history at 100 entries" (drop the oldest, ideally with a ring buffer), "states are huge" (store commands or patches), and "don't undo one letter at a time" (group changes).
Where frontend code uses it
The same idea in real UI code
- Text editors like CodeMirror and ProseMirror keep undo and redo stacks of changes, grouping changes made close together in time.
- Redux's undo recipe and the
redux-undolibrary keeppast,present, andfuture, which are these two stacks around the present state. - Design tools, form builders, and drawing canvases use the same structure; the browser's back and forward buttons behave the same way, where visiting a new page clears the forward list.