How to create a collaborative online whiteboard with React and Liveblocks
In this 25-minute tutorial, we’ll be building a collaborative whiteboard app using React 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 realtime 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. If you’re using a state-management library such as Redux or Zustand, we recommend reading one of our dedicated whiteboard tutorials:
A live demo and the source code for this guide are in our examples.
Install Liveblocks into your project
Install Liveblocks packages
Create a new app with create-react-app
:
Then run the following command to install the Liveblocks packages:
Connect to Liveblocks servers
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_
.
Let’s now add a new file src/liveblocks.config.js
in our application to create
a Liveblocks client using the public key as shown below.
Connect to a Liveblocks room
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.
Instead of using the client directly, we’re going to use
createRoomContext
from @liveblocks/react
to create a
RoomProvider
and hooks to
make it easy to consume from our components.
You might be wondering why we’re creating our Providers and Hooks with
createRoomContext
instead of importing them directly from
@liveblocks/client
. This allows TypeScript users to define their Liveblocks
types once in one unique location—allowing them to get a great autocompletion
experience when using those hooks elsewhere.
We can now import the RoomProvider
directly from our
src/liveblocks.config.js
file.
Every component wrapped inside RoomProvider
will have access to the special
React hooks we’ll be using to interact with this room.
Create a canvas
We’re going to use a LiveMap
to store a map of shapes inside the room’s storage. A LiveMap
is a type of
storage that Liveblocks provides. A LiveMap
is like a JavaScript map, but its
items are synced in realtime across different clients. Even if multiple users
insert or delete items simultaneously, the LiveMap
will still be consistent
for all users in the room.
Initialize the storage with the initialStorage
prop on the RoomProvider
.
We’re going to use the useMap
hook to get the map of shapes previously created. Let’s re-export it from
scr/liveblocks.config
.
useMap
returns null
while it’s still connecting to Liveblocks, so we can
rely on that to show a loading state first. Once it’s connected, we can 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.
Insert rectangles
Our whiteboard is currently empty, and there’s no way to add rectangles. Let’s create a button that adds a randomly placed rectangle to the board.
We’ll use shapes.set
to add a new rectangle to the LiveMap
.
Add selection
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.
To do this, we’ll employ the
useMyPresence
hook to
store a user’s selected shape, and then the
useOthers
hook to find other
users’ selected shapes. But first, let’s re-export those hooks from
src/liveblocks.config
like we did previously with useMap
.
We can now use those hooks directly in our application by importing them from
src/liveblocks.config.js
.
Delete rectangles
Now that users can select rectangles, we can add a button that allow deleting rectangles too.
We’ll use shapes.delete
to remove the selected shape from the LiveMap
, and
then reset the user’s selection.
Move rectangles
Let’s move some rectangles!
To allow users to move rectangles, we’ll update the x
and y
properties using
shapes.set
when a user drags a shape.
Multiplayer undo/redo
With Liveblocks, you can enable multiplayer undo/redo in just a few lines of code.
We’ll add the useHistory
hook and call history.undo()
and history.redo()
when the user presses "undo"
or "redo". By default, only modifications made to the storage are added to the
undo/redo history. In this example, that means all changes made to shapes
(because it’s a LiveMap
)."
Like we did with the others hooks, re-export it from your
src/liveblocks.config
.
To undo a change to presence we need to add the { addToHistory: true }
option
to setPresence
.
Pause and resume history
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 realtime.
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.
Batch insert and selection
In one click we can undo both creating a rectangle, and selecting a rectangle, by merging both changes into a single history state.
We can add the useBatch
hook
to do this, by calling all our changes within the batch()
callback argument.
Like we did with the others hooks, re-export it from your
src/liveblocks.config
.
Better performance and conflict resolution
If two users modify the same rectangle at the same time, it’s possible that
problems will occur. To provide better conflict resolution, we can use the
CRDT-like LiveObject
to
store each rectangle’s data. Liveblocks storage can contain nested data
structures, and in our example, shapes
is a LiveMap
containing multiple
LiveObject
items.
To get this working, we need to use room.subscribe
, to rerender the
rectangles when the nested data structure updates:
Re-export useRoom
from src/liveblocks.config
to get the current Room
from
your components.
Voilà! We have a working collaborative whiteboard app, with persistent data storage.
Summary
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.