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.
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 realtime.
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 realtime, 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.
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.
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.
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.
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.
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 realtime to keep everyone in sync, otherwise the feeling of being together in the same room starts to deteriorate.
Instead, what we want to do is show intermediary states as they happen.
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.
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.
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.
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.
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 realtime 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()
.
And if you are using Liveblocks with React, the same utilities can be accessed
through the useHistory
hook
from
@liveblocks/react
.
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.
Ready to get started?
Join developers who use Liveblocks to build world‑class collaborative experiences.