How to create a collaborative to-do list with React and Liveblocks
In this 15-minute tutorial, 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, Next.js and TypeScript. 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
Then run the following command to install the Liveblocks packages:
Let’s now add a new file
liveblocks.config.ts at the root of your app 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 collaborative experience, multiple users must be connected to the same room.
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
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 exporting the suspense version of our hooks in
liveblocks.config.ts. Because Next.js uses server-side rendering by default,
we must wrap
RoomProvider children in our
component. Liveblocks does not connect through WebSocket when running on server,
ClientSideSuspense ensure its children are not rendered on the server. More
information can be found
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
liveblocks.config.ts. Because we’re using
Suspense, we’ll be exporting all
our hooks from the
useOthers takes a selector function that receives an array,
containing information about each user. We can get the current user count from
the length of that array. Add the following code to
pages/index.tsx, and open
your app in multiple windows to see it in action.
For a tidier UI, replace the content of
styles/globals.css file with the
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
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.
Let’s define a new type
Presence with the property
liveblocks.config.ts to ensure all our presence hooks are typed properly.
We can use the
useUpdateMyPresence hook to change with the current user’s
presence. But first, let’s re-export it from
liveblocks.config.ts like we did
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
SomeoneIsTyping, and use this to display a message whilst anyone else
is typing. To check if anyone is typing, we’re iterating through
returning true if
isTyping is true for any user.
We also need to make sure that we pass an
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
clients. Even if multiple users insert, delete, or move items simultaneously,
LiveList will still be consistent for all users in the room.
First, let's declare a new type
liveblocks.config.ts, like we did
Presence. This will ensure that our storage hooks are properly typed.
Go back to
Page to initialize the storage with the
initialStorage prop on
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
useStorage allows us to select part of the storage from the
root level. We
can find our
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
useMutation gives you access to the storage root, a
here we can use
LiveObject.get to retrieve the
todos list, then use
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.