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.
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!
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.
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.
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.
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.
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.