In this 15-minute guide, we’ll be building a collaborative to-do list using React and Liveblocks. As users edit the list, changes will be automatically synced and persisted, allowing for a list that updates in real-time across clients. Users will also be able to see who else is currently online, and when another user is typing.
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 to-do list tutorials:
The source code for this guide is available on GitHub.
Create a new app with create-react-app
:
Then run the following command to install the Liveblocks packages:
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 starts 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.
Let’s now add a new file src/liveblocks.config.js
to our application to create
a Liveblocks client, using the public key as shown below.
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—providing a helpful autocompletion experience
when using those hooks elsewhere.
We can now import the RoomProvider
directly from our liveblocks.config.js
file. Every component wrapped inside RoomProvider
will have access to the
React hooks we’ll be using to interact with this room.
You may also notice that we’re wrapping our app in a Suspense boundary in this example. Doing so enables the suspense version of our hooks—an option we recommend using for simpler component code.
If your project uses server-side rendering (e.g. in a Next.js project) you must
use ClientSideSuspense
from @liveblocks/react instead of the normal Suspense
component from React. More information can be found
here.
Now that Liveblocks is set up, we can start using the hooks to display how many users are currently online.
We’ll be doing this by adding useOthers
, a selector hook that provides us
information about which other users are online. First, let’s re-export it from
scr/liveblocks.config
. Because we’re using Suspense
, we’ll be exporting all
our hooks from the suspense
property.
useOthers
takes a selector function that receives an array, others
,
containing information about each user. We can get the current user count from
the length of that array. Add the following code to src/App.js
, and open your
app in multiple windows to see it in action.
For a tidier UI, here’s some styling you can import into your index.js
file.
Next, we’ll add some code to show a message when another user is typing.
Any online user could start typing, and we need to keep track of this, so it’s
best if each user holds their own isTyping
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, the selected shape in a design tool, or in this case, if they’re currently typing or not.
We can use the useUpdateMyPresence
hook to change with the current user’s
presence. But first, let’s re-export it from src/liveblocks.config
like we did
previously with useOthers
.
We can then call updateMyPresence
whenever we wish to update the user’s
current presence, in this case whether they’re typing or not.
Now that we’re keeping track of everyone’s state, we can create a new component
called SomeoneIsTyping
, and use this to display a message whilst anyone else
is typing. To check if anyone is typing, we’re iterating through others
and
returning true if isTyping
is true for any user.
We also need to make sure that we pass an initialPresence
for isTyping
to
RoomProvider
.
To-do list items will be stored even after all users disconnect, so we won’t be using presence to store these values. For this, we need something new.
We’re going to use a LiveList
to store the list of todos inside the room’s
storage, a type of storage that Liveblocks provides. A LiveList
is similar to
a JavaScript array, but its items are synced in real-time across different
clients. Even if multiple users insert, delete, or move items simultaneously,
the LiveList
will still be consistent for all users in the room.
Go back to App
to initialize the storage with the initialStorage
prop on the
RoomProvider
.
We’re going to use the useStorage
hook to get the list of todos previously
created. And once again, we’ll need to first re-export it from your
src/liveblocks.config
.
useStorage
allows us to select part of the storage from the root
level. We
can find our todos
LiveList
at root.todos
, and we can map through our list
to display each item.
To modify the list, we can use the useMutation
hook. This is a hook that
works similarly to useCallback
, with a dependency array, allowing you to
create a reusable storage mutation. Before we use this, first we need to
re-export this from ./liveblocks.config.js
.
useMutation
gives you access to the storage root, a LiveObject
. From
here we can use LiveObject.get
to retrieve the todos
list, then use
LiveList.push
and LiveList.delete
to modify our todo list. These
functions are then passed into the appropriate events.
Voilà! We have a working collaborative to-do list, 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.