@liveblocks/react
provides you with React bindings for
our real-time collaboration APIs, built on top of WebSockets. Read our
getting started guides to learn more.
Creates a RoomProvider
and a set of typed hooks. We recommend using it in
liveblocks.config.ts
and re-exporting your typed hooks like below.
When using Comments, anything that is linked to a
user (comments, mentions, etc.) will reference their ID. To display user
information based on IDs, you can provide a resolver function to the
resolveUsers
option in createRoomContext
.
This resolver function will receive a list of user IDs and should return a list of user objects of the same size and in the same order.
Once that’s done, components will show user names and avatars accordingly.
To enable creating mentions in Comments, you can
provide a resolver function to the resolveMentionSuggestions
option in
createRoomContext
.
This resolver function will receive the mention currently being written (e.g.
when writing “@jane”, text
will be jane
) and should return a list of user
IDs matching that text. This function will be called every time the text changes
but with some debouncing.
Makes a Room
available in the component hierarchy below. Joins the room when
the component is mounted, and automatically leaves the room when the component
is unmounted.
initialPresence
- The initial Presence to use for the User currently
entering the Room. Presence data belongs to the current User and is readable
to all other Users in the room while the current User is connected to the
Room. Must be serializable to JSON.initialStorage
(optional) - The initial Storage structure to create when a
new Room is entered for the first time. Storage data is shared and belongs to
the Room itself. It persists even after all Users leave the room, and is
mutable by every client. Must either contain Live structures (e.g.
new LiveList()
, new LiveObject({ a: 1 })
, etc.) or be serializable to
JSON.autoConnect
(optional) - Whether or not the room should automatically
connect to Liveblocks servers when the RoomProvider is mounted. By default
it’s set to typeof window !== "undefined"
, meaning the RoomProvider attempts
to connect to Liveblocks servers only on the client side.The initialPresence
, initialStorage
and autoConnect
props are ignored
after the first render, so changes to the initial value argument won’t have an
effect.
Returns the Room
of the nearest RoomProvider
above in the React
component tree.
Listen to potential room connection errors.
Returns the current WebSocket connection status of the room, and will re-render your component whenever it changes.
The possible value are: initial
, connecting
, connected
, reconnecting
, or
disconnected
.
Calls the given callback when an “others” event occurs. Possible event types are:
enter
– A user has entered the room.leave
– A user has left the room.reset
– The others list has been emptied. This is the first event that
occurs when the room is entered. It also occurs when you’ve lost connection to
the room.update
– A user’s presence data has been updated.Calls the given callback in the exceptional situation that a connection is lost and reconnecting does not happen quickly enough.
This event allows you to build high-quality UIs by warning your users that the app is still trying to re-establish the connection, for example through a toast notification. You may want to take extra care in the mean time to ensure their changes won’t go unsaved.
When this happens, this callback is called with the event lost
. Then, once the
connection restores, the callback will be called with the value restored
. If
the connection could definitively not be restored, it will be called with
failed
(uncommon).
The lostConnectionTimeout
client option will determine how quickly this
event will fire after a connection loss (default: 5 seconds).
Automatically unsubscribes when the component is unmounted.
For a demonstration of this behavior, see our connection status example.
Return the presence of the current user, and a function to update it. Automatically subscribes to updates to the current user’s presence.
Note that the updateMyPresence
setter function is different to the setter
function returned by React’s useState
hook. Instead, you can pass a partial
presence object to updateMyPresence
, and any changes will be merged into the
current presence. It will not replace the entire presence object.
This is roughly equal to:
updateMyPresence
accepts an optional argument to add a new item to the
undo/redo stack. See room.history
for more information.
Returns a setter function to update the current user’s presence.
Use this if you don’t need the current user’s presence in your component, but
you need to update it (e.g. live cursor). It’s better to use
useUpdateMyPresence
because it won’t subscribe your component to get
rerendered when the presence updates.
Note that the updateMyPresence
setter function is different to the setter
function returned by React’s useState
hook. Instead, you can pass a partial
presence object to updateMyPresence
, and any changes will be merged into the
current presence. It will not replace the entire presence object.
updateMyPresence
accepts an optional argument to add a new item to the
undo/redo stack. See room.history
for more information.
Returns the current user once it is connected to the room, and automatically subscribes to updates to the current user.
The benefit of using a selector is that it will only update your component if that particular selection changes. For full details, see how selectors work.
👉 A Suspense version of this hook is also available, which will never
return null
.
Extracts data from the list of other users currently in the same Room, and automatically subscribes to updates on the selected data. For full details, see how selectors work.
The others
argument to the useOthers
selector function is an immutable
array of Users.
👉 A Suspense version of this hook is also available, which will never
return null
.
One caveat with this API is that selecting a subset of data for each user
quickly becomes tricky. When you want to select and get updates for only a
particular subset of each user’s data, we recommend using the
useOthersMapped
hook instead, which is optimized for this use case.
When called without arguments, returns the user list and updates your component whenever anything in it changes. This might be way more often than you want!
Extract data using a selector for every user in the room, and subscribe to all changes to the selected data. A Suspense version of this hook is also available.
The key difference with useOthers
is that the selector (and the optional
comparison function) work at the item level, like doing a .map()
over the
others array.
Returns an array where each item is a pair of [connectionId, data]
. For
pragmatic reasons, the results are keyed by the connectionId
, because in most
cases you’ll want to iterate over the results and draw some UI for each, which
in React requires you to use a key={connectionId}
prop.
Returns an array of connection IDs (numbers), and rerenders automatically when
users join or leave. This hook is useful in particular in combination with the
useOther
(singular) hook, to implement high-frequency rerendering of
components for each user in the room, e.g. cursors. See the useOther
(singular) documentation below for a full usage example.
Roughly equivalent to:
👉 A Suspense version of this hook is also available.
Extract data using a selector for one specific user in the room, and subscribe to all changes to the selected data. A Suspense version of this hook is also available.
The reason this hook exists is to enable the most efficient rerendering model for high-frequency updates to other’s presences, which is the following structure:
👉 A Suspense version of this hook is also available, which will never
return null
.
Returns a callback that lets you broadcast custom events to other users in the room.
Listen to custom events sent by other people in the room via
useBroadcastEvent
. Provides the event
along with the connectionId
of
the user that sent the message. If an event was sent from the
Broadcast to a room
REST API, connectionId
will be -1
.
The user
property will indicate which User instance sent the message. This
will typically be equal to one of the others in the room, but it can also be
null
in case this event was broadcasted from the server, using the
Broadcast Event API.
Automatically unsubscribes when the component is unmounted.
The room’s storage is a conflicts-free state that multiple users can edit at the same time. It persists even after everyone leaves the room. Liveblocks provides 3 data structures that can be nested to create the state that you want.
LiveObject
- Similar to JavaScript object. Use this for storing records
with fixed key names and where the values don’t necessarily have the same
types. For example, a Person
with a name
(string) and an age
(number)
field.
If multiple clients 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. Use this for indexing values that
all have the same structure. For example, to store an index of Person
values
by their name. If multiple users update the same property simultaneously, the
last modification received by the Liveblocks servers is the winner.
@liveblocks/react
provides a set of hooks that let you interact with the
room’s storage.
Extracts data from Liveblocks Storage state and automatically subscribes to updates to that selected data. For full details, see how selectors work.
The root
argument to the useStorage
selector function is an immutable copy
of your entire Liveblocks Storage tree. Think of it as the value you provided in
the initialStorage
prop at the RoomProvider
level, but then
(recursively) converted to their “normal” JavaScript equivalents (objects,
arrays, maps) that are read-only.
From that immutable root
, you can select or compute any value you like. Your
component will automatically get rerendered if the value you return differs from
the last rendered value.
This hook returns null
while storage is still loading. To avoid that, use the
Suspense version.
Returns the LiveObject
associated with the provided top-level key in your
Storage root. The key should be a LiveObject
instance, as populated in the
initialStorage
prop at the RoomProvider
level.
The hook returns null
while the storage is loading, unless you use the
Suspense version.
⚠️ Caveat 1: This hook can only be used to select top-level keys. You cannot select nested values from your Storage with this hook.
⚠️ Caveat 2: The hook only triggers a rerender if direct keys or values of
the LiveObject
are updated, but it does not trigger a rerender if any of
its nested values get updated.
👉 A Suspense version of this hook is also available, which will never
return null
.
Returns the LiveMap
associated with the provided top-level key in your
Storage root. The key should be a LiveMap
instance, as populated in the
initialStorage
prop at the RoomProvider
level.
The hook returns null
while the storage is loading, unless you use the
Suspense version.
⚠️ Caveat 1: This hook can only be used to select top-level keys. You cannot select nested values from your Storage with this hook.
⚠️ Caveat 2: The hook only triggers a rerender if direct entries of the
LiveMap
are updated, but it does not trigger a rerender if a nested
value gets updated.
Returns the LiveList
associated with the provided top-level key in your
Storage root. The key should be a LiveList
instance, as populated in the
initialStorage
prop at the RoomProvider
level.
The hook returns null
while the storage is loading, unless you use the
Suspense version.
⚠️ Caveat 1: This hook can only be used to select top-level keys. You cannot select nested values from your Storage with this hook.
⚠️ Caveat 2: The hook only triggers a rerender if direct items in the
LiveList
are updated, but it does not trigger a rerender if a nested
value gets updated.
👉 A Suspense version of this hook is also available, which will never
return null
.
Returns a function that batches modifications made during the given function. All the modifications are sent to other clients in a single message. All the modifications are merged in a single history item (undo/redo). All the subscribers are called only after the batch is over.
Returns the room’s history. See Room.history
for more information.
Returns a function that undoes the last operation executed by the current client. It does not impact operations made by other clients.
Returns a function that redoes the last operation executed by the current client. It does not impact operations made by other clients.
Returns whether there are any operations to undo.
Returns whether there are any operations to redo.
Creates a callback function that lets you mutate Liveblocks state.
To make the example above more flexible and work with any color, you have two options:
Both are equally fine, just a matter of preference.
Alternatively, you can add extra parameters to your callback function:
For convenience, the mutation context also receives self
and others
arguments, which are immutable values reflecting the current Presence state,
in case your mutation depends on it.
For example, here’s a mutation that will delete all the shapes selected by the current user.
Mutations are automatically batched, so when using useMutation
there’s no need
to use useBatch
, or call room.batch()
manually.
If you are using ESLint in your project, and are using
the React hooks plugin,
we recommend to add a check for "additional hooks", so that it will also check
the dependency arrays of your useMutation
calls:
Returns the threads within the current room.
👉 A Suspense version of this hook is also available, which will never return a loading state and will throw when there's an error.
Returns user info from a given user ID.
👉 A Suspense version of this hook is also available, which will never return a loading state and will throw when there's an error.
Returns a function that creates a thread with an initial comment, and optionally some metadata.
Returns a function that edits a thread’s metadata.
Returns a function that adds a comment to a thread.
Returns a function that edits a comment’s body.
Returns a function that deletes a comment. If it is the last non-deleted comment, the thread also gets deleted.
Returns a function that adds a reaction to a comment.
Returns a function that removes a reaction from a comment.
Compares two values shallowly. This can be used as the second argument to selector based functions to loosen the equality check:
The default way selector results are
compared is by checking referential equality (===
). If your selector returns
computed arrays (like in the example above) or objects, this will not work.
By passing shallow
as the second argument, you can “loosen” this check. This
is because shallow
will shallowly compare the members of an array (or values
in an object):
Please note that this will only do a shallow (one level deep) check. Hence the name. If you need to do an arbitrarily deep equality check, you’ll have to write a custom equality function or use a library like Lodash for that.
The concepts and behaviors described in this section apply to all of our
selector hooks: useStorage
, useSelf
, useOthers
,
useOthersMapped
, and useOther
(singular).
In a nutshell, the key behaviors for all selector APIs are:
Let’s go over these traits and responsibilities in the next few sections.
The received input to all selector functions is a read-only and immutable top level context value that differs for each hook:
useStorage((root) => ...)
receives the Storage rootuseSelf((me) => ...)
receives the current useruseOthers((others) => ...)
receives a list of other users in the roomuseOthersMapped((other) => ...)
receives each individual other user in the
roomuseOther(connectionId, (other) => ...)
receives a specific user in the roomFor example, suppose you have set up Storage in the typical way by setting
initialStorage
in your RoomProvider
to a tree that describes your app’s
data model using LiveList
, LiveObject
, and LiveMap
. The "root" argument
for your selector function, however, will receive an immutable and read-only
representation of that Storage tree, consisting of "normal" JavaScript
datastructures. This makes consumption much easier.
Internally, these read-only trees use a technique called structural sharing. This means that between rerenders, if nodes in the tree did not change, they will guarantee to return the same memory instance. Selecting and returning these nodes directly is therefore safe and considered a good practice, because they are stable references by design.
Selectors you write can return any value. You can use it to “just” select nodes from the root tree (first two examples above), but you can also return computed values, like in the last two examples.
One important rule is that selector functions must return a stable result to be efficient. This means calling the same selector twice with the same argument should return two results that are referentially equal. Special care needs to be taken when filtering or mapping over arrays, or when returning object literals, because those operations create new array or object instances on every call (the reason why is detailed in the next section).
(root) => root.animals
is stableLiveblocks guarantees this. All nodes in the Storage tree are stable references as long as their contents don’t change.
(root) => root.animals.map(...)
is not stableBecause .map()
creates a new array instance every time. You’ll need to use
shallow
here.
(root) => root.animals.map(...).join(", ")
is stableBecause .join()
ultimately returns a string and all primitive values are
always stable.
If your selector function doesn’t return a stable result, it will lead to an
explosion of unnecessary rerenders. In most cases, you can use a shallow
comparison function to loosen the check:
If your selector function constructs complex objects, then a shallow
comparison may not suffice. In those advanced cases, you can provide your own
custom comparison function, or use _.isEqual
from Lodash.
Selectors effectively automatically subscribe your components to updates to the selected or computed values. This means that your component will automatically rerender when the selected value changes.
Using multiple selector hooks within a single React component is perfectly fine. Each such hook will individually listen for data changes. The component will rerender if at least one of the hooks requires it. If more than one selector returns a new value, the component still only rerenders once.
Technically, deciding if a rerender is needed works by re-running your selector
function (root) => root.child
every time something changes inside Liveblocks
storage. Anywhere. That happens often in a busy multiplayer app! The reason why
this is still no problem is that even though root
will be a different value on
every change, root.child
will not be if it didn’t change (due to how
Liveblocks internally uses structural sharing).
Only once the returned value is different from the previously returned value, the component will get rerendered. Otherwise, your component will just remain idle.
Consider the case:
And the following timeline:
root.animals
initially is ["🦁", "🦊", "🐵"]
.root.animals
gets re-evaluated, but it still returns the same
(unchanged) array instance.root.animals
gets re-evaluated, and now it returns ["🦁", "🦊"]
.Starting with 0.18, you can use Liveblocks hooks with React’s Suspense.
The benefit of using Suspense with Liveblocks is that the hooks will no longer
return null
when Liveblocks is still loading. Instead, you can let your
Suspense boundary handle the still-loading case centrally by showing the
fallback state.
This can turn code like this:
Into:
To start using the special “Suspense versions” of our hooks, you can simply
import them from under the suspense
key in the object returned by the
createRoomContext
call. All the hooks have the same name, so switching is
easy.
Now all these hooks will no longer return null
values while Liveblocks is
still loading, and instead suspend rendering.
Next, you’ll have to wrap your app in a <Suspense>
boundary where you want to
centrally handle the loading state.
Normally, this looks like:
The above works fine if your app only runs in a browser. If your project uses server-side rendering (e.g. Next.js app), then the above solution won’t work and throw errors. In that case, please read on.
One caveat with the Suspense hooks is that they cannot be run on the server side, as they will throw an error. So you’ll need to avoid rendering those components on the server side.
Fortunately, this is easy to avoid with a tiny helper component we ship with our
React package. It can be used in place of React’s default <Suspense>
, almost
as a drop-in replacement. This helper will make sure to always render the
fallback
on the server side, and only ever rendering its children on the
client side, when it’s no problem.
React context provider that lets you use all the presence and storage hooks of
@liveblocks/react
.