Undo, Redo, and the Command Pattern

Design patterns are useful tools for us as developers, because they give us the terminology to discuss recurring ideas and patterns that show up in our code. Unfortunately, they’re also often explained in theoretical, object-oriented terms, and as a result, they can be difficult to apply in our daily programming practice unless we’ve seen them before. I want to try and unpack the command pattern, how we ended up using it in a recent project of ours, and how it might be implemented in JavaScript.

When we build a complex application, we can describe it as a series of different states. We start in some initial state, then some event occurs (say, the user clicks on a button), and we transform the application data to create a new state. That might look something like this:

Image

A diagram showing multiple nodes labelled "state" in a line. Between each pair of states, there is an arrow that travels through a black box (labelled "black box") indicating how a transition between states occurs.

Here, the steps between the different states is described as some amount of code. That code is not necessarily easy to observe — it might make all sorts of changes to the state, and it might have side-effects as well.

Now let’s imagine we want to introduce undo/redo to our application. When the user presses “undo”, we need to step back in time through our application — instead of moving to the next state, we want to move to the previous one. Similarly, we have a “redo” action that will move us forwards a step on our line of states.

Image

The same diagram as before, but with arrows indicating how "undo" operations take us to a previous state, and "redo" operations take us to the next state.

The problem we now need to solve is this: how do we move back to the previous state?

One easy answer is store all the states we visit along the way. If you’re familiar with the Redux ecosystem, you might have used tools like redux-undo that handle this automatically. You write the code in the black boxes, and the library automatically maintains the different states and switches between them.

Another, similar option might be to instead store diffs of the different states. Whenever we create a new state, we compare it to the old state and store a record of all the changes that have been made. This can be more memory efficient (the diffs are likely much smaller than a copy of the whole state would be), but calculating the diff in the first place can be less efficient.

These are both very good options that work in a lot of cases. If you’re managing your application state somewhere centrally, typically using the Flux pattern, then it’s usually very easy to use one of these approaches and get undo/redo with almost no extra work.

This is a blog post about a different approach.

Why You Might Want a Different Approach

There are two main reasons why the above approaches might not work out for you.

The first reason is that both approaches assume that your state is managed in a single, central place. There are some architectures where that is very easy to achieve, but as your state gets larger and more complicated, it can often be easier to break the state into smaller pieces and work with those pieces independently. This allows more flexibility, but it also means that you no longer have a single source of truth.

The second reason is that your state transitions might affect things other than the state – or in other words, have side-effects. At first, it might feel like the obvious solution is to avoid the side-effects in the first place, but often the side-effects are the things we want. Consider a classic counter with a button to increment the internal state. When I click the button and change the state, I also want to change the UI to reflect the new state of the counter. This is one of the key side-effects that we need to deal with.

In a recent project that inspired this post, our application was large, and therefore we had split it up into multiple controllers. Each controller worked independently (and so could be tested/understood independently), and managed its own state. At the same time, the application used SolidJS to manage the UI. In SolidJS, as the internal state updates, side-effects are run which directly update the DOM as needed. This produces very efficient DOM-updates (the famous “fine-grained reactivity”), but means that we can’t treat our state purely as data any more — we need to understand how it’s changing as well.

In the end, we opted for the command pattern. Let’s explore what that looks like.

The Command Pattern

In our original example, we treated the code that moved us between different states as a black box. As developers, we could look into it and understand how it went, but we didn’t have the tools to introspect it, and undo or replay parts of it.

In the command pattern, we instead describe each transition via a combination of commands and data. Commands are the different actions that we can do to our state — for a todo app, we might have commands like “add todo”, “delete todo”, “mark todo as done”, and so on. The data is the specific arguments that we’ll pass to the command. The result looks something like this:

Image

A series of nodes labelled "state" are laid out left to right. Between each pair of nodes, there is an arrow connecting the nodes that travels through a box split into two sections. The sections are labelled "command" and "data", indicating how each transition can be defined by a particular command and an associated piece of data.

If we go back to our todo app, when we click one of the “done” checkboxes in our UI, we would call the “mark todo as done” command with some data (probably the ID of the todo we’re interested in), and this function would update the internal data store and fire off the necessary side effects to produce the next state.

We can’t quite undo anything yet, though. For that, we need the second feature of commands, which is that they know how to undo themselves. The “add todo” command has a function which adds a todo to the state and updates the UI, but it also has a function which removes that same todo as well. So each command knows how to do and undo its action.

Image

A series of nodes labelled "state" are laid out left to right. Between each pair of nodes, pointing right to left, there is an arrow indicating the transition between the different states. The arrow passes through a box split into two parts labelled "command prime" and "data", indicating that it is possible to transition through the states in reverse by applying the command's inverse operation.

With this, we can build our undo/redo system. Every time we run a command, we also record:

  • Which command was run
  • What data it was run with

When we want to undo some action, we call the command’s undo function, and pass it the same data it had before. It will revert all the changes it made before, and leave us exactly in the state we were in before.

If we go back to our reasons for a different approach, we can see that the command pattern neatly solves both of them:

  • Each component of the code can define its own commands (in the same way it might define its own methods or functions), meaning we can still treat each component in isolation.
  • The command is a function, which means it can update the state and call any side effects as necessary.

Show Me the Code

Let’s look at how we might the logic of a todo app in command form.

First, let’s define what our command actually is. In other languages, we might use classes, but in TypeScript we can get away with a relatively simple object:

type Command<Data> = {
  do: (data: Data) => void;
  undo: (data: Data) => void;
};

We’re also going to need our history. For that, we need a list of actions that can be undone, and a list of actions that can be redone after that. We’ll also provide a function for pushing a new entry onto the lists, because there’s a bit of logic there that we don’t want to have to repeat everywhere:

type CommandPair = { command: Command<any>, data: any };
const undoableCommands: CommandPair[] = [];
const redoableCommands: CommandPair[] = [];

function pushCommand<Data>(command: Command<Data>, data: Data) {
  command.do(data);
  undoableCommands.push({ command, data });
  redoableCommands.length = 0;
}

Now we can define the commands specific to our todo system. Note that this won’t be all the possible commands, although feel free to think about what other commands might be necessary yourself.

const todoStore = []; // super simple store, definitely production-ready

// here, the data is just the string of the todo
// (we assume that all todos are unique for simplicity)
const createTodo: Command<string> = {
  do: (data) => todoStore.push({ todo: data, done: false }),
  undo: (data) => todoStore = todoStore.filter(t => t.todo !== data),
}

// here, we store the old (`prev`) and the new (`next`) states
// of the `.done` attribute, so that we can step forwards and
// backwards through the history
const setTodoState: Command<{todo: string, prev: boolean, next: boolean}> = {
  do: (data) => {
    const todo = todoStore.find(t => t.todo === data.todo);
    todo.done = data.next;
  },
  undo: (data) => {
	  const todo = todoStore.find(t => t.todo === data.todo);
	  todo.done = data.prev;
	},
}

In practice, I’d probably wrap those commands in functions that call the pushCommand function internally, just to make things a little bit nicer to use, but we can skip that for now. Finally, we need our undo and redo functions. Now we’ve got our commands, these are really easy to implement: just call the relevant functions on the commands with the attached data.

function undo() {
  const cmd = undoableCommands.pop();
  if (!cmd) return false; // nothing to undo
  cmd.command.undo(cmd.data);
  redoableCommands.push(cmd);
  return true; // successfully undid an action
}

function redo() {
  const cmd = redoableCommands.pop();
  if (!cmd) return false; // nothing to undo
  cmd.command.do(cmd.data);
  undoableCommands.push(cmd);
  return true; // successfully redid an action
}

Other Considerations

The undo system we’ve implemented here is very bare-bones, to try and explore the basic ideas around commands, but there’s plenty more that we could add here.

One thing that a lot of applications very quickly need is the ability to batch commands together, so that a single “undo” operation will undo a number of commands at once. This is important if each command should only affect its own slice of the state, but a particular operation affects multiple slices.

Another consideration is the ability to update commands. Consider an operation to resize an image. As I drag my cursor around, the UI should update smoothly, but when I stop resizing and press undo, I want to undo the whole resize operation, not just one part of it. One way of doing that is by adding a kind of upsertCommand function next to the pushCommand one, which creates a new entry in the history if there wasn’t one before, or else updates the previous entry with the new data.

It’s also important to be aware of the limitations of the command pattern. One of the benefits of the Flux architecture or tools like Redux is that they create a strict framework where it’s difficult to accidentally mutate data or end up in an unexpected state. Commands, on the other hand, are much more flexible, but in turn you need to ensure that all changes to the state really are taking place inside commands, and not in arbitrary functions.

Conclusion

The command pattern is a useful way of defining undoable state transitions to an application. It allows us to split an application up into different controllers or slices of data. It also allows us to apply side-effects that will be consistently be reapplied and undone as the user undoes and redoes their history. Hopefully, this article has helped you think about when and how you might apply the command pattern in your own tools and applications.