In this guide, you’ll learn how to use integrate Liveblocks into your React +
Redux application. The @liveblocks/redux
package provides a
store enhancer
to turn selected parts of your Redux store into a real‑time shared state between
clients. This approach makes it easy to convert an existing non-collaborative
application into one. It also enables you to keep your front-end architecture
without sprinkling Liveblocks code all over your code base.
If you’re not using a state-management library, we recommend reading our dedicated React guide below:
You can also follow our step-by-step tutorial to learn how to use Liveblocks.
Create a new app with create-react-app
and
once the installation is complete, run the following command to install
Liveblocks dependencies:
You’ll need an API key in order to use Liveblocks. Create a Liveblocks account to get your API key. It should start with pk_.
Then, create a client with createClient
and set your public key as shown
below.
Use our enhancer
in your store setup and pass the client created previously as
the configuration. This will add a new state called liveblocks
to your store,
enabling you to interact with our Presence and Storage APIs.
Then setup your redux store at the top of your React tree.
A room is the virtual space where people collaborate. To create a multiplayer experience, you’ll need to connect your users to a Liveblocks room.
Dispatch enterRoom
to start syncing parts of your store to the Liveblocks room
and dispatch leaveRoom
to stop the synchronization. With React useEffect
,
you can automatically enter and leave a Liveblocks room as your component is
mounting and unmounting.
If you want to list all the people connected to the room, you can use
state.liveblocks.others
to get an array of the other users in the room.
To create immersive multiplayer experiences, it’s helpful for each person in the room to share their real‑time state with other connected users. That real‑time state often corresponds to a cursor position or even the item a user has currently selected. We call this concept "Presence".
For instance, to share the cursor’s position in real‑time with others, we’re
going to add a new presenceMapping
option to our enhancer
to specify which
part of the state maps to the current user’s presence
.
Then you can dispatch an action like in any redux app and we will broadcast this cursor to everyone in the room.
Get people’s cursor positions with
liveblocks.others.map(user => user.presence?.cursor)
. It’s worth noting that a
user presence can be undefined
.
As opposed to the presence
, some collaborative features require that every
user interacts with the same piece of state. For example, in Google Docs, it is
the paragraphs, headings, images in the document. In Figma, it’s all the shapes
that make your design. That’s what we call the room’s storage
.
The room’s storage is a conflicts-free state that multiple users can edit at the same time. It is persisted to our backend even after everyone leaves the room. Liveblocks provides custom data structures inspired by CRDTs that can be nested to create the state that you want.
LiveObject
- Similar to JavaScript object. If multiple users update the
same property simultaneously, the last modification received by the Liveblocks
servers is the winner.LiveList
- An ordered collection of items synchronized across clients.
Even if multiple users add/remove/move elements simultaneously, LiveList will
solve the conflicts to ensure everyone sees the same collection of items.LiveMap
- Similar to a JavaScript Map. If multiple users update the same
property simultaneously, the last modification received by the Liveblocks
servers is the winner.When using our redux integration you don't need to interact directly with these
data structures. Our enhancer synchronize your store with our data structures
based on the storageMapping
configuration.
Here is an example to explain how it works under the hood. Imagine you have the following store:
With this setup, the room's storage
root is :
If you update your store by dispatching setFirstName("Pierre")
, the enhancer
will do root.set("firstName", "Pierre")
for you and update the store of all
the users currently connected to the room. The enhancer compares the previous
state and the new state to detect changes and patch our data structures
accordingly.
The reverse process happens when receiving updates from other clients; the enhancer patches your immutable state.
When entering a room with enterRoom
, the enhancer fetches the room's storage
from our server and patches your store. If this is the first time you're
entering a room, the storage will be empty. enterRoom
takes an additional
argument to initialize the room's storage.
Implementing undo/redo when multiple users can edit the app state simultaneously is quite complex!
When only one user can edit the app state, undo/redo acts like a "time machine"; undo/redo replaces the current app state with an app state that was already be seen by the user.
When multiple users are involved, undo or redo can lead to an app state that no one has seen before. For example, let's imagine a design tool with two users editing the same circle.
{ radius: "10px", color: "yellow" }
color
to blue
=> { radius: "10px", color: "blue" }
radius
to 20px
=> { radius: "20px", color: "blue" }
{ radius: "20px", color: "yellow" }
A yellow circle with a radius of 20px in a completely new state. Undo/redo in a multiplayer app does not act like a "time machine"; it only undoes local operation.
The good news is that room.history.undo
and room.history.redo
takes
that complexity out of your hands so you can focus on the core features of your
app.
Access these two functions from the client like below so you can easily bind them to keyboard events (⌘+Z/⌘+⇧+Z on Mac and Ctrl+Z/Ctrl+Y on Windows) or undo and redo buttons in your application..
Some applications require skipping intermediate states from the undo/redo history. 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. But they should be shared with the rest of the room to create a great experience.
room.history.pause
and room.history.resume
lets you skip these
intermediate states. To go back to our design tool example, the sequence of
calls would look like that:
room.history.pause
to skip future operations from the historyroom.history.resume
At this point, if the user calls room.history.undo
, the rectangle will go back
to its initial position.