Guides

How to build undo/redo in a multiplayer environment

Notoriously tough to build, undo/redo in a multiplayer environment is even more fraught with difficulty. In this article, we will take you on a behind-the-scenes exclusive, explaining one of the most complex developer issues for multiplayer apps.

Picture of Guillaume Salles
Guillaume Salles
@guillaume_slls
Picture of Marc Bouchenoire
Marc Bouchenoire
@marcbouchenoire
Picture of Steven Fabre
Steven Fabre
@stevenfabre
How to build undo/redo in a multiplayer environment

Undo/redo is a feature that is extensively used and essential in content creation software today. Building this functionality in a non‑collaborative environment is theoretically pretty straight forward, but still notoriously difficult to build. This becomes even more complex in a multiplayer world where many people can make changes in real‑time.

But what makes developing the undo/redo feature in a collaborative environment so complex, and how can you tackle it properly?

The main challenges in developing an undo/redo feature in a collaborative environment involve making undo/redo client‑specific, displaying all changes from other users in real‑time, and keeping the state of other features in the document other than just the previous contents.

Let’s dive in to how we handle multiplayer undo/redo at Liveblocks.

Undo/redo needs to be local to each client

One of the most commonly used approaches to handle undo/redo is to save the application’s previous states and rewind through those when the user hits undo. This technique is also known as the memento pattern and has been popularized by the undo history implementation of popular state-management library Redux.

An interactive visualization displaying a canvas and its multiple states across time.

In contrast to non‑collaborative situations, preserving the previous application state of that user and returning to it does not work. That is because several users can change the state of the document, and undoing may delete work done by others.

An interactive visualization displaying a canvas with two users and how rewinding to previous states leads to conflicts.

B
A

See? Someone just lost their work. It’s not a great multiplayer experience, and it could be much worse in a real-world scenario where someone could lose hours of their time in the blink of an eye.

Instead of storing previous states, as most non‑collaborative applications do, it’s better to have undo/redo processes that are client‑specific, so that a user may only undo or redo their own changes. To do that, we can rely on the command pattern where each user’s opposite command is stored in order to apply it later when people use undo and redo.

An interactive visualization displaying a canvas and individual timelines for each user to avoid conflicts.

B
A

We’re on the right track now! However, there are several drawbacks to this new command-based model; under certain circumstances, conflicts can be difficult to resolve.

An interactive visualization displaying a canvas and how some uncommon conflicts can still happen.

B
A

Tricky, right? Should we recreate the deleted shape? Should we ignore the undo? Should we undo the next operation in the stack?

Figma and Google Slides both handle this the same way — nothing happens. However, Pitch handles this situation by entering into an invalid state.

Video showing what Figma, Google Slides, and Pitch handle undo on shapes that do not exist anymore.

Thankfully, by showing people’s presence with things like selection and cursors, this is something that is unlikely to happen in a real‑world scenario. That’s why we at Liveblocks chose to handle this in the same way as Figma and Google Slides do. If you have any ideas on how to improve this, please let us know!

Intermediary commands need to be grouped

Operations that affect multiple users must be shown in real‑time to keep everyone in sync, otherwise the feeling of being together in the same room starts to deteriorate.

An interactive visualization displaying two side-by-side canvases and users not seeing the same changes happening in sync.

A
B

Instead, what we want to do is show intermediary states as they happen.

An interactive visualization displaying two side-by-side canvases and users seeing the same changes at all times.

A
B

Isn’t that much better? But with that comes undo/redo challenges. Let’s look at what would happen if someone were to undo changes after dragging a layer.

An interactive visualization displaying a canvas and a user having to undo many times after moving a shape.

A

Certainly not the finest experience. Imagine having to undo 20 times to go back to where you were a few seconds ago. That would be tiring!

Had we been in a non‑collaborative environment using the memento pattern, this could easily be solved by skipping intermediary states. In a multiplayer command-based undo/redo system, we can also solve this by pausing and resuming the history stack at the right time. But in order for this to work when a user hits undo, we need to apply all the commands that happened in‑between at once.

In this scenario, we would pause the history on mouse down when the user begins dragging and resume it on mouse up when they are done dragging.

An interactive visualization displaying a canvas and a user having to undo only once after moving a shape.

A

As you can see in the illustration above, the user was able to swiftly return to their initial state, keeping everything flowing smoothly.

Undo/redo should affect more than just the document’s content

Depending on the use case, the state of features like user selection, user page selection, user zoom setting, etc. could be included in the undo/redo stack to provide a great experience. A good example of an actual use case can be seen in Figma, where users can navigate between pages and then undo to go back to the previously selected page. States like these must be included in the history stack so that when undoing or redoing operations, the current user’s selection or setting is consistent with the page’s current state.

In a design tool, for example, if a user previously selected a shape, you want to ensure that the shape stays selected when they undo. The user’s selection state is vital to a great undo/redo experience because it maintains the flow of the system and keeps them completely immersed in the work they are doing.

An interactive visualization displaying two side-by-side canvases and users losing their selections after others hit undo or redo.

A
B

The experience above isn’t ideal, right? The user must now select the layer again to choose the layer. This may not seem like much, but when you’re in the flow of creating something, it’s important that the tool never gets in the way of what the user is attempting to create.

An interactive visualization displaying two side-by-side canvases and users keeping their selections after others hit undo or redo.

A
B

Much better—but this is difficult to implement. That’s why at Liveblocks, we’ve built APIs that enable developers to include user-specific features like selection in the undo/redo stack of the products they’re building. This ensures people using those products can have a best-in-class experience that always keep them in flow.

Let Liveblocks be your unsung hero

While we focused on use cases for creative tools, it’s worth noting that these patterns are applicable to any multiplayer products—so you should now have enough knowledge to design your own multiplayer undo/redo solution.

If you don’t want the hassle, you’re also welcome to use Liveblocks directly, and we’ll keep working to build the real‑time collaborative infrastructure we’ve always wanted.

When using Liveblocks, a few history utilities can be accessed through room.history from @liveblocks/client to implement multiplayer undo/redo the right way in seconds: undo(), redo(), pause(), and resume().

const { undo, redo, pause, resume } = room.history;
function onPointerDown(event) { pause();}
function onPointerMove(event) { shape.update({ x: event.clientX, y: event.clientY, });}
function onPointerUp() { resume();}

And if you are using Liveblocks with React, the same utilities can be accessed through the useHistory hook from @liveblocks/react.

const { undo, redo, pause, resume } = useHistory();

At Liveblocks, we build APIs for developers to create multiplayer applications, and we love to solve complex problems. Liveblocks is in your corner—let us be your behind-the-scenes champions so you can focus on your core features instead.

If you’re passionate about making the web more collaborative, and you love these kinds of engineering and UX challenges, we’re hiring! You can also follow along and contribute on GitHub.