Intro to Writing Undo/Redo Systems in JavaScript

Quinn Branscombe
Sep 20, 2020 · 6 min read

When designing applications focused on the creation or modification of data, like text or image editors, for example, a common desire for your end-user is the ability to undo or redo their actions. It’s an important consideration, because the knowledge that steps can be safely and easily undone gives your users confidence in using your app.

So, you’ve decided to try integrating an undo system into your project, but you’ve never written anything like that before. How do they work? Where do you even start? This article aims to give you a little direction by introducing you to how undo systems work and how to write one.

An “Undoable” Counter

Let’s start this off with a simple (and extremely common) example: a counter!

Mind-blowing, right? This code defines a function for generating counters that can be incremented or decremented using the increment and decrement methods! …yeah, there’s not much to it. And its behaviour is similarly predictable:

While nothing to write home about, this code provides the perfect place to begin exploring undo operations. It’s easy to understand what it’s doing, and it has state data that is subject to change. Let’s see if we can add an undo function to it!

The easiest way we could implement undo operations would be to keep track of the counter’s value whenever it changes, and add it to a “history” array that we can use to revisit previous states. We’ll also keep a pointer to the specific index of the array that represents the present value of the counter in a variable called “position”. With both a history array and a position pointer, we no longer need to keep a separate value variable, and will replace it with a function that returns the element of history at the current position.

Illustration of a State History based Undo system

The implementation might look like this:

With this in place, all we need to do to enable undo and redo for our state changes is decrementing and incrementing the history position! Knowing this, we can go ahead and add the undo and redo methods:

Simple, right? All that’s left is to modify the increment and decrement methods to actually push new values to history and update the position accordingly. We’ll add a new method, setValue, to assist with that:

The setValue function accepts a new value, removes any states ahead of the current position in history (making a new change after a series of undos should clear any existing ‘future’ states), pushes the new value to history, and points position to the new state. The increment and decrement methods now use it, as well as the new value method, to perform the same tasks they did before.

All in all, our new “undoable” counter should look like this:

And we can try it out with some simple driver code:

Success! Our new object is capable of remembering its past states and revisiting them with its undo and redo methods!

A More Robust Approach

The previous approach is a good introduction to history traversal, and works well enough for tracking a single, simple variable. However, trying to track more complex data exposes some shortcomings. What if you wanted to undo changes made to an object this way? Would you deep copy the entire object into the history at every change? If the object contains a significant amount of data, making many small changes to it could quickly start to eat up all your available memory!

To avoid that, we’ll turn to a common programming design pattern: the Command pattern. Explaining the pattern in-depth is beyond the scope of this article, but for our purposes you can think of it as taking the actions you wish to perform on your data and turning them into objects that can be passed around, stored, and executed at any time by a controller.

Abstracting the actions into command objects allows us to store a history of all the changes we’ve made to an object, rather than a history of that object’s states.

Illustration of a Command History based Undo system

Let’s take this new concept and make it familiar by applying it to a similar situation as our last example.

So in this case we have yet another counter. This time, however, its state is an object with multiple properties: a name and a count. It also doesn’t have any built-in increment or decrement methods. We’ll be writing commands to take on that responsibility!

In the case of an undo/redo system, a command should be an object that at the very least contains two methods: execute and undo. As the names imply, execute performs an action on the data, and undo restores it to its previous state.

Let’s see what increment might look like when implemented as a command:

When a new increment command is created, it takes in the counter object it is modifying as a parameter, and stores the current value of the counter in a variable called previousCount. Its execute method increases the value of the counter’s count by one, and its undo method restores its value to the state stored in previousCount.

Given the simplicity of incrementing a counter, the undo method could also be implemented as simply decrementing it. However, storing the previous state of any changed variables is a more flexible solution that can apply to more complex operations, so I’ve elected to demonstrate that technique here.

Decrementing is essentially the same but with subtraction instead of addition in its execute method:

But this is only half of the story. If we’re making commands, we also have to have a way to store them, apply them, and manage a command history to enable a chain of undos and redos! Let’s define a command manager to take care of that for us.

Whoa! That’s a bunch of new stuff! Some of it should look familiar, though. The managing of history and position, as well as the history traversal in the undo and redo methods, are pretty much the same as what we did in the last example. So we’ll focus instead on what’s new here.

INCREMENT, DECREMENT, and commands are all constants used to ease the process of selecting a particular command to generate. The string constants prevent string input errors, and the commands object exists to take the place of a switch statement, allowing you to access a particular command generator function by its associated string. You can learn more about using objects in place of switches here, if you like.

createCommandManager takes an object as a parameter in its creation to use as a target to apply its commands to (not always necessary for the pattern, but it fits well with the rest of the implementation here). It returns an object with three methods: doCommand, undo, and redo.

doCommand is where the magic happens. First, it clears any future commands left by undos, much like setValue does in our last example. It then checks to see if the command string it was passed exists in the commands object, and if so, it creates a new command object from it, targetting target. It pushes the new command to the history array, then executes it, thereby applying its changes to target.

As mentioned earlier, undo and redo are very similar to the previous example, but now that we’re dealing with a history of commands, undo runs the command’s undo (and redo runs execute) to update the object’s state.

Now that we’ve got all the pieces of the puzzle together, let’s try out some driver code and see it all in action!

Yes! It works as expected! We’re able to increment and decrement our counter and undo changes to it via the command manager, all without touching or having to keep track of the name variable!

If you’ve been following along, feel free to pat yourself on the back, because you’ve learned how to time travel! At least in the context of your application state. :)

If you want to learn more about the command pattern and undo systems, feel free to check out these resources I used in writing this article:

For beginners by developers

We share the knowledge of students at Lighthouse Labs Full Stack Web Development program.