In this 25-minute guide, we’ll be building a collaborative whiteboard app using React, Zustand and Liveblocks. As users add and move rectangles in a canvas, changes will be automatically synced and persisted, allowing for a canvas that updates in real-time across clients. Users will also be able to see other users selections, and undo and redo actions.
This guide assumes that you’re already familiar with React and Zustand. If you’re not using Zustand, we recommend reading one of our dedicated whiteboard tutorials:
The source code for this guide is available on github.
Create a new app with create-react-app
:
To start a plain JavaScript project, you can omit the --template typescript
flag.
Then install the Liveblocks packages and Zustand:
In order to use Liveblocks, we’ll need to sign up and get an API key.
Create an account, then navigate to
the dashboard to find your public key. It should start
with pk_
.
With a secret key, you can control who can access the room. it’s more secure but you need your own back-end endpoint. For this tutorial, we’ll go with a public key. For more info, see the authentication guide.
Create a new file src/store.ts
and initialize the Liveblocks client with your
public API key. Then add our
liveblocks
middleware to
your store configuration.
Liveblocks uses the concept of rooms, separate virtual spaces where people can collaborate. To create a multiplayer experience, multiple users must be connected to the same room.
Our middleware injected the object liveblocks
to the store. Inside that
object, the first methods that we are going to use are
enterRoom
and
leaveRoom
.
In our main component, we want to connect to the Liveblocks room when the component does mount, and leave the room when it unmounts.
Whiteboard shapes will be stored even after all users disconnect, so we will use Liveblocks storage to persist them.
Add a shapes
property to your store, and tell the middleware to sync and
persist them with Liveblocks.
To achieve that, we are going to use the middleware option
storageMapping: { shapes: true }
.
It means that the part of the state named shapes
should be automatically
synced with Liveblocks Storage.
Afterwards, we draw the shapes in our canvas. To keep it simple for the tutorial, we are going to only support rectangle.
Place the following within src/App.css
, and then you will be able to
insert rectangular shapes into the whiteboard.
Currently our whiteboard is empty, and there’s no way to add rectangles. Let’s create a button that adds a randomly placed rectangle to the board.
Add a new function to your store that randomly insert a rectangle on the board.
Then add a button to call this function from the board.
We can use Liveblocks to display which shape each user is currently selecting, in this case by adding a border to the rectangles. We’ll use a blue border to represent the local user, and green borders for remote users.
Any online user could select a shape, and we need to keep track of this, so it’s
best if each user holds their own selectedShape
property.
Luckily, Liveblocks uses the concept of presence to handle these temporary states. A user’s presence can be used to represent the position of a cursor on screen, or in this case the selected shape in a design tool.
We want to add some data to our Zustand store, selectedShape
will contain the
selected shape id. selectedShape
will be set when the user select or insert a
rectangle.
The middleware option
presenceMapping: { selectedShape: true }
means that we want to automatically sync the part of the state named
selectedShape
to Liveblocks Presence.
Update your App
and Rectangle
components to show if a shape is selected by
the current user or someone else in the room.
Now that users can select rectangles, we can add a button that allow deleting rectangles too.
Add a deleteShape
to remove the selected shape from shapes
, and then reset
the user’s selection:
Let’s move some rectangles!
To allow users to move rectangles, we’ll update the x
and y
properties of
the selected shape when a user drags it:
With Liveblocks, you can enable multiplayer undo/redo in just a few lines of code.
Add two buttons to the toolbar and bind them to
room.history.undo
and
room.history.redo
.
These functions only impact modifications made to the room’s storage.
When a user moves a rectangle, a large number of actions are sent to Liveblocks and live synced, enabling other users to see movements in real-time.
The problem with this is that the undo button returns the rectangle to the last intermediary position, and not the position where the rectangle started its movement. We can fix this by pausing storage history until the move has completed.
We’ll use
history.pause
to
disable adding any positions to the history stack while the cursors moves, and
then call
history.resume
afterwards.
By default, presence udpates are not added to the room’s history. Let’s add the current user selection to the room’s history to improve our undo/redo behavior.
To accomplish that, use
room.updatePresence
with the option addToHistory
to update selectedShape
. Liveblocks middleware
will update store selectedShape
for you.
Voilà! We have a working collaborative whiteboard app, with persistent data storage.
In this tutorial, we’ve learnt about the concept of rooms, presence, and others. We've also learnt how to put all these into practice, and how to persist state using storage too.
You can see some stats about the room you created in your dashboard.