It’s probably my third day writing here. I’m not trying to keep a daily streak, but I get genuinely excited when I come across good code, so I end up wanting to share it right away.
Some background
In a personal project I’ve been working on, I spent the past week trying to find a clean, generic way to implement undo/redo without ending up with a bunch of hardcoded states. I wanted something that could manage itself instead of turning into a mess over time.
I’m glad to say I found two solid approaches. You can probably guess that from the title since this is Part 1 😄
In this part, I’ll walk through one approach that I found really elegant—the one recommended by Redux.
The idea
The core idea is simple. Instead of just storing your current state, you store a bit of history along with it.
{
past: Array<T>,
present: T,
future: Array<T>
}
-
past→ everything that came before -
present→ your current state -
future→ everything you’ve undone
Undo and redo then become operations where you just move state between these three.
Example: Todo list
Let’s walk through a simple todo list to make this concrete.
Start with an empty list:
past: []
present: []
future: []
Now you add a todo: "Buy milk"
past: [[]]
present: ["Buy milk"]
future: []
Add another todo: "Walk dog"
past: [[], ["Buy milk"]]
present: ["Buy milk", "Walk dog"]
future: []
Add one more: "Read book"
past: [[], ["Buy milk"], ["Buy milk", "Walk dog"]]
present: ["Buy milk", "Walk dog", "Read book"]
future: []
Now hit undo:
past: [[], ["Buy milk"]]
present: ["Buy milk", "Walk dog"]
future: [["Buy milk", "Walk dog", "Read book"]]
Hit undo again:
past: [[]]
present: ["Buy milk"]
future: [["Buy milk", "Walk dog"], ["Buy milk", "Walk dog", "Read book"]]
And if you redo:
past: [[], ["Buy milk"]]
present: ["Buy milk", "Walk dog"]
future: [["Buy milk", "Walk dog", "Read book"]]
This works because you’re not doing anything fancy—you’re just moving snapshots around.
Making it reusable
The really nice part is that this logic doesn’t have to live inside your reducer.
You can extract it into a reusable wrapper, or as Redux calls it, a reducer enhancer.
function undoable(reducer) {
const initialState = {
past: [],
present: reducer(undefined, {}),
future: []
}
return function (state = initialState, action) {
const { past, present, future } = state
switch (action.type) {
case 'UNDO': {
const previous = past[past.length - 1]
const newPast = past.slice(0, past.length - 1)
return {
past: newPast,
present: previous,
future: [present, ...future]
}
}
case 'REDO': {
const next = future[0]
const newFuture = future.slice(1)
return {
past: [...past, present],
present: next,
future: newFuture
}
}
default: {
const newPresent = reducer(present, action)
if (present === newPresent) {
return state
}
return {
past: [...past, present],
present: newPresent,
future: []
}
}
}
}
}
Using it
You write your reducer like normal:
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, action.text]
default:
return state
}
}
Then wrap it:
const undoableTodos = undoable(todos)
And now it supports undo and redo out of the box:
store.dispatch({ type: 'ADD_TODO', text: 'Use Redux' })
store.dispatch({ type: 'ADD_TODO', text: 'Implement Undo' })
store.dispatch({ type: 'UNDO' })
One small gotcha
Since your state is now wrapped, you’ll need to access state.present instead of state.
You can also use:
-
past.lengthto know if undo is possible -
future.lengthto know if redo is possible
Why this is nice
What I like about this approach is how composable it is. You can choose which reducers need undo and layer this on top without changing your existing logic.
Your original reducer stays untouched, and undo/redo becomes something you add, not something you bake in.
Also, if you don’t want to implement this yourself, redux-undo already does exactly this.
This article is heavily inspired by the official Redux documentation, so I’d definitely recommend checking that out as well.
In the next part, I’ll go into the approach I actually ended up using, and why it worked better for my setup. I haven’t written it yet, but I’ll link it here when I do.
Thanks for reading
Until next time, 👋
Top comments (0)