How to use Liveblocks multiplayer undo/redo with React

In this guide, we’ll be learning how to use Liveblocks multiplayer undo/redo with React using the hooks from the @liveblocks/react package.

This guide uses TypeScript. Liveblocks can definitely be used without TypeScript. We believe typings are helpful to make collaborative apps more robust, but if you’d prefer to skip the TypeScript syntax, feel free to write your code in JavaScript.

Multiplayer undo/redo

Implementing undo/redo in a multiplayer environment is notoriously complex, but Liveblocks provides functions to handle it for you. useUndo and useRedo return functions that allow you to undo and redo the last changes made to your app.

import { useUndo, useRedo } from "./liveblocks.config";
function App() { const undo = useUndo(); const redo = useRedo();
return ( <> <button onClick={() => undo()}>Undo</button> <button onClick={() => redo()}>Redo</button> </> );}

An example of this in use would be a button that updates the current firstName of a scientist. Every time a Liveblocks storage change is detected, in this case .set being called, it’s stored. Pressing the undo button will change the name back to its previous value.

import { useState } from "react";import { useMutation, useUndo } from "./liveblocks.config";
function YourComponent() { const [text, setText] = useState(""); const undo = useUndo();
const updateName = useMutation(({ storage }, newName) => { storage.get("scientist").set("firstName", newName); });
return ( <> <input type="text" value={text} onChange={(e) => setText(e.target.value)} /> <button onClick={() => updateName(text)}>Update Name</button> <button onClick={() => undo()}></button> </> );}

Multiplayer undo/redo is much more complex that it sounds—if you’re interested in the technical details, you can find more information in our interactive article: How to build undo/redo in a multiplayer environment.

Pause and resume history

Sometimes it can be helpful to pause undo/redo history, so that multiple updates are reverted with a single call.

For example, let’s consider a design tool; when a user drags a rectangle, the intermediate rectangle positions should not be part of the undo/redo history, otherwise pressing undo may only move the rectangle one pixel backwards. However, these small pixel updates should still be transmitted to others, so that the transition is smooth.

useHistory is a hook that allows us to pause and resume history states as we please.

import { useHistory } from "./liveblocks.config";
function App() { const { resume, pause } = useHistory();
return <Rectangle onDragStart={() => pause()} onDragEnd={() => resume()} />;}

Presence history

By default, undo/redo only impacts the room storage—there’s generally no need to use it with presence, for example there’s no reason to undo the position of a user’s cursor. However, occasionally it can be useful.

If we explore the design tool scenario, the currently selected rectangle may be stored in a user’s presence. If undo is pressed, and the rectangle is moved back, it would make sense to remove the user’s selection on that rectangle.

To enable this, we can use the addToHistory option when updating the user’s presence.

import { useUpdateMyPresence } from "./liveblocks.config";
function App() { const updateMyPresence = useUpdateMyPresence();
return ( <Rectangle onClick={(rectangleId) => updateMyPresence({ selected: rectangleId }, { addToHistory: true }) } /> );}

This also works in mutations with setMyPresence.

import { useMutation } from "./liveblocks.config";
const updateSelected = useMutation(({ setMyPresence }, rectangleId) => { setMyPresence({ selected: rectangleId }, { addToHistory: true });});