API Reference - @liveblocks/react

@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.

Room

createRoomContext

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.

liveblocks.config.ts
import { createClient } from "@liveblocks/client";import { createRoomContext } from "@liveblocks/react";
const client = createClient({ // publicApiKey: "", // authEndpoint: "/api/liveblocks-auth", // throttle: 100,});
// Presence represents the properties that exist on every user in the Room// and that will automatically be kept in sync. Accessible through the// `user.presence` property. Must be JSON-serializable.type Presence = { // cursor: { x: number, y: number } | null, // ...};
// Optionally, Storage represents the shared document that persists in the// Room, even after all users leave. Fields under Storage typically are// LiveList, LiveMap, LiveObject instances, for which updates are// automatically persisted and synced to all connected clients.type Storage = { // animals: LiveList<string>, // ...};
// Optionally, UserMeta represents static/readonly metadata on each user, as// provided by your own custom auth back end (if used). Useful for data that// will not change during a session, like a user's name or avatar.// type UserMeta = {// id?: string, // Accessible through `user.id`// info?: Json, // Accessible through `user.info`// };
// Optionally, the type of custom events broadcast and listened to in this// room. Use a union for multiple events. Must be JSON-serializable.// type RoomEvent = {};
// Optionally, when using Comments, ThreadMetadata represents metadata on// each thread. Can only contain booleans, strings, and numbers.// export type ThreadMetadata = {// resolved: boolean;// quote: string;// time: number;// };
export const { RoomProvider, useMyPresence, useStorage, /* ...all the other hooks you’re using... */} = createRoomContext< Presence, Storage /* UserMeta, RoomEvent, ThreadMetadata */>(client);

RoomProvider

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.

import { LiveList, LiveMap, LiveObject } from "@liveblocks/client";import { RoomProvider } from "./liveblocks.config";
function AppRoot() { return ( <RoomProvider id="my-room" // 😎 Replace with your own data! initialPresence={{ cursor: { x: 0, y: 0 } }} // 😎 Replace with your own data! initialStorage={() => ({ animals: new LiveList(["🦁", "🦊", "🐵"]),
mathematician: new LiveObject({ firstName: "Ada", lastName: "Lovelace", }),
fruitsByName: new LiveMap([ ["apple", "🍎"], ["banana", "🍌"], ["cherry", "🍒"], ]), })} > {/* children */} </RoomProvider> );}

useRoom

Returns the Room of the nearest RoomProvider above in the React component tree.

import { useRoom } from "./liveblocks.config";
const room = useRoom();

useErrorListener

Listen to potential room connection errors.

import { useErrorListener } from "./liveblocks.config";
useErrorListener((error) => { switch (error.code) { case -1: // Authentication error break;
case 4001: // Could not connect because you don't have access to this room break;
case 4005: // Could not connect because room was full break;
case 4006: // The room ID has changed, get the new room ID (use this for redirecting) const newRoomId = error.message; break;
default: // Unexpected error break; }});

useStatus

Returns the current WebSocket connection status of the room, and will re-render your component whenever it changes.

import { useStatus } from "./liveblocks.config";
const status = useStatus();

The possible value are: initial, connecting, connected, reconnecting, or disconnected.

useOthersListener

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.
function App() {  useOthersListener(({ type, user, others }) => {    switch (type) {      case "enter":        // `user` has entered the room        break;
case "leave": // `user` has left the room break;
case "update": // Presence for `user` has updated break;
case "reset": // Others list has been emptied break; } });}

useLostConnectionListener

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).

import { toast } from "my-preferred-toast-library";
function App() { useLostConnectionListener((event) => { switch (event) { case "lost": toast.warn("Still trying to reconnect..."); break;
case "restored": toast.success("Successfully reconnected again!"); break;
case "failed": toast.error("Could not restore the connection"); break; } });}

Automatically unsubscribes when the component is unmounted.

For a demonstration of this behavior, see our connection status example.

Liveblocks

createLiveblocksContext

Creates a LiveblocksProvider and a set of typed hooks. We recommend using it in liveblocks.config.ts and re-exporting your typed hooks like below.

While createRoomContext offers APIs for interacting with rooms (e.g. Presence, Storage, and Comments), createLiveblocksContext offers APIs for interacting with Liveblocks features that are not tied to a specific room (e.g. Notifications).

liveblocks.config.ts
import { createClient } from "@liveblocks/client";import { createRoomContext, createLiveblocksContext } from "@liveblocks/react";
const client = createClient({ // publicApiKey: "", // authEndpoint: "/api/liveblocks-auth", // throttle: 100,});
// ...
export const { RoomProvider } = createRoomContext(client);
export const { LiveblocksProvider, useInboxNotifications, /* ...all the other hooks you’re using... */} = createLiveblocksContext(client);

LiveblocksProvider

Makes Liveblocks features outside of rooms (e.g. Notifications) available in the component hierarchy below.

Unlike RoomProvider, LiveblocksProvider doesn’t call Liveblocks servers when mounted, so it can (and should) live higher in the component tree of your app.

import { LiveList, LiveMap, LiveObject } from "@liveblocks/client";import { LiveblocksProvider } from "./liveblocks.config";
function AppRoot() { return <LiveblocksProvider>{/* children */}</LiveblocksProvider>;}

Presence

useMyPresence

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.

import { useMyPresence } from "./liveblocks.config";
const [myPresence, updateMyPresence] = useMyPresence();updateMyPresence({ x: 0 });updateMyPresence({ y: 0 });
// At the next render, "myPresence" will be equal to "{ x: 0, y: 0 }"

This is roughly equal to:

const myPresence = useSelf((me) => me.presence);const updateMyPresence = useUpdateMyPresence();

updateMyPresence accepts an optional argument to add a new item to the undo/redo stack. See room.history for more information.

updateMyPresence({ selectedId: "xxx" }, { addToHistory: true });

useUpdateMyPresence

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.

import { useUpdateMyPresence } from "./liveblocks.config";
const updateMyPresence = useUpdateMyPresence();
updateMyPresence({ y: 0 });

updateMyPresence accepts an optional argument to add a new item to the undo/redo stack. See room.history for more information.

updateMyPresence({ selectedId: "xxx" }, { addToHistory: true });

useSelf

Returns the current user once it is connected to the room, and automatically subscribes to updates to the current user.

import { useSelf } from "./liveblocks.config";
const currentUser = useSelf();// {// connectionId: 1,// presence: { cursor: { x: 27, y: -8 } },// }
const currentUser = useSelf((me) => me.presence.cursor);// { x: 27, y: -8 }

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.

useOthers

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.

// ✅ Rerenders only if the number of users changesconst numOthers = useOthers((others) => others.length);
// ✅ Rerenders only if someone starts or stops typingconst isSomeoneTyping = useOthers((others) => others.some((other) => other.presence.isTyping));
// ✅ Rerenders only if actively typing users are updatedconst typingUsers = useOthers( (others) => others.filter((other) => other.presence.isTyping), shallow // 👈);

👉 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.

// ❌ Mapping is hard to get right with this hookconst cursors = useOthers(  (others) => others.map((other) => other.presence.cursor),  shallow);
// ✅ Better to use useOthersMappedconst cursors = useOthersMapped((other) => other.presence.cursor);

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!

const others = useOthers(); // ⚠️ Caution, might rerender often!// [//   { connectionId: 2, presence: { cursor: { x: 27, y: -8 } } },//   { connectionId: 3, presence: { cursor: { x: 0, y: 19 } } },// ]

useOthersMapped

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.

// Example 1const others = useOthersMapped((other) => other.presence.cursor);// [//   [2, { x: 27, y: -8 }],//   [3, { x: 0, y: 19 }],// ]
// Example 2const others = useOthersMapped( (other) => ({ avatar: other.info.avatar, isTyping: other.presence.isTyping, }), shallow // 👈);
// [// [2, { avatar: 'https://...', isTyping: true }],// [3, { avatar: null, isTyping: false }],// ]

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.

const others = useOthersMapped((other) => other.presence.cursor);
// In JSXreturn ( <> {others.map(([connectionId, cursor]) => ( <Cursor key={connectionId} x={cursor.x} y={cursor.y} /> ))} </>);

useOthersConnectionIds

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.

useOthersConnectionIds(); // [2, 4, 7]

Roughly equivalent to:

useOthers((others) => others.map((other) => other.connectionId), shallow);

👉 A Suspense version of this hook is also available.

useOther

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.

// ✅ Rerenders when this specific user’s isTyping changes (but not when their cursor changes)const isTyping = useOther(  3, // User with connectionId 3  (user) => user.presence.isTyping);

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:

Cursors.tsx
const Cursors =  // (1) Wrap parent component in a memo and make sure it takes no props  React.memo(function () {    const othersConnectionIds = useOthersConnectionIds(); // (2)    return (      <>        {othersConnectionIds.map((connectionId) => (          <Cursor            key={connectionId} // (3)            connectionId={connectionId}          />        ))}      </>    );  });
Cursor.tsx
function Cursor({ connectionId }) {  const { x, y } = useOther(connectionId, (other) => other.presence.cursor); // (4)  return <Cursor x={x} y={y} />;}
  1. Makes sure this whole component tree will never rerender beyond the first time.
  2. Makes sure the parent component only rerenders when users join/leave.
  3. Makes sure each cursor remains associated to the same connection.
  4. Makes sure each cursor rerenders whenever its data changes only.

👉 A Suspense version of this hook is also available, which will never return null.

Broadcast

useBroadcastEvent

Returns a callback that lets you broadcast custom events to other users in the room.

import { useBroadcastEvent } from "./liveblocks.config";
// On client Aconst broadcast = useBroadcastEvent();broadcast({ type: "EMOJI", emoji: "🔥" });
// On client BuseEventListener(({ event, user, connectionId }) => { // ^^^^ Will be Client A if (event.type === "EMOJI") { // Do something }});

useEventListener

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.

import { useEventListener } from "./liveblocks.config";
// On client Aconst broadcast = useBroadcastEvent();broadcast({ type: "EMOJI", emoji: "🔥" });
// On client BuseEventListener(({ event, user, connectionId }) => { // ^^^^ Will be Client A if (event.type === "EMOJI") { // Do something }});

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.

Storage

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.

useStorage

Extracts data from Liveblocks Storage state and automatically subscribes to updates to that selected data. For full details, see how selectors work.

// ✅ Rerenders if todos (or their children) changeconst items = useStorage((root) => root.todos);
// ✅ Rerenders when todos are added or deletedconst numTodos = useStorage((root) => root.todos.length);
// ✅ Rerenders when the value of allDone changesconst allDone = useStorage((root) => root.todos.every((item) => item.done));
// ✅ Rerenders if any _unchecked_ todo items changeconst uncheckedItems = useStorage( (root) => root.todos.filter((item) => !item.done), shallow // 👈);

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.

useObject

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.

index.tsx
import { LiveObject } from "@liveblocks/client";import { RoomProvider } from "./liveblocks.config";import { Component } from "./Component";
export function App() { return ( <RoomProvider id="my-room-id" initialStorage={{ mathematician: new LiveObject({ firstName: "Ada", lastName: "Lovelace", }), }} > <Component /> </RoomProvider> );}
Component.tsx
import { useObject } from "./liveblocks.config";
function Component() { const object = useObject("mathematician");
if (object == null) { return <div>Loading...</div>; }
object.get("firstName"); // => "Ada" object.get("lastName"); // => "Lovelace"}

👉 A Suspense version of this hook is also available, which will never return null.

useMap

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.

index.tsx
import { LiveMap } from "@liveblocks/client";import { RoomProvider } from "./liveblocks.config";import { Component } from "./Component";
export function App() { return ( <RoomProvider id="my-room-id" initialStorage={{ fruitsByName: new LiveMap([ ["apple", "🍎"], ["banana", "🍌"], ["cherry", "🍒"], ]), }} > <Component /> </RoomProvider> );}
Component.tsx
import { useMap } from "./liveblocks.config";
function Component() { const fruitsByName = useMap("fruitsByName");
if (fruitsByName == null) { return <div>Loading...</div>; }
fruitsByName.get("cherry"); // => "🍒"}

useList

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.

index.tsx
import { LiveList } from "@liveblocks/client";import { RoomProvider } from "./liveblocks.config";import { Component } from "./Component";
export function App() { return ( <RoomProvider id="my-room-id" initialStorage={{ animals: new LiveList(["🦁", "🦊", "🐵"]), }} > <Component /> </RoomProvider> );}
Component.tsx
import { useList } from "./liveblocks.config";
function Component() { const animals = useList("animals");
if (animals == null) { return <div>Loading...</div>; }
animals.toArray(); // => ["🦁", "🦊", "🐵"]}

👉 A Suspense version of this hook is also available, which will never return null.

useBatch

Returns a function that batches Storage and Presence modifications made during the given function. Each modification is grouped together, which means that other clients receive the changes as a single message after the batch function has run. Every modification made during the batch is merged into a single history item (undo/redo).

import { useBatch } from "./liveblocks.config";
const batch = useBatch();batch(() => { // All modifications made in this callback are batched});

Note that batch cannot take an async function.

// ❌ Won't workbatch(async () => /* ... */);
// ✅ Will workbatch(() => /*... */);

useHistory

Returns the room’s history. See Room.history for more information.

import { useHistory } from "./liveblocks.config";
const { undo, redo, pause, resume } = useHistory();

useUndo

Returns a function that undoes the last operation executed by the current client. It does not impact operations made by other clients.

import { useUndo } from "./liveblocks.config";
const undo = useUndo();

useRedo

Returns a function that redoes the last operation executed by the current client. It does not impact operations made by other clients.

import { useRedo } from "./liveblocks.config";
const redo = useRedo();

useCanUndo

Returns whether there are any operations to undo.

import { useCanUndo, useUpdateMyPresence } from "./liveblocks.config";
const updateMyPresence = useUpdateMyPresence();const canUndo = useCanUndo();
updateMyPresence({ y: 0 });
// At the next render, "canUndo" will be true

useCanRedo

Returns whether there are any operations to redo.

import { useCanRedo, useUndo, useUpdateMyPresence } from "./liveblocks.config";
const updateMyPresence = useUpdateMyPresence();const undo = useUndo();const canRedo = useCanRedo();
updateMyPresence({ y: 0 });undo();
// At the next render, "canRedo" will be true

useMutation

Creates a callback function that lets you mutate Liveblocks state.

const fillWithRed = useMutation(  // Mutation context is passed as the first argument  ({ storage, setMyPresence }) => {    // Mutate Storage    storage.get("shapes").get("circle1").set("fill", "red");    //                                   ^^^
// ...or Presence setMyPresence({ lastUsedColor: "red" }); }, []);
// JSXreturn <button onClick={fillWithRed} />;

To make the example above more flexible and work with any color, you have two options:

  1. Close over a local variable and adding it to the dependency array, or
  2. Have it take an extra callback parameter.

Both are equally fine, just a matter of preference.

With dependency arrays

// Local state maintained outside Liveblocksconst [currentColor, setCurrentColor] = useState("red");
const fillWithCurrentColor = useMutation( ({ storage, setMyPresence }) => { storage.get("shapes").get("circle1").set("fill", currentColor); setMyPresence({ lastUsedColor: currentColor }); }, [currentColor] // Works just like it would in useCallback);
// JSXreturn <button onClick={fillWithCurrentColor} />;

With extra callback parameters

Alternatively, you can add extra parameters to your callback function:

const fill = useMutation(  // Note the second argument  ({ storage, setMyPresence }, color: string) => {    storage.get("shapes").get("circle1").set("fill", color);    setMyPresence({ lastUsedColor: color });  },  []);
// JSXreturn <button onClick={() => fill("red")} />;// ^^^^^^^^^^^ Now fill takes a color argument

Depending on current presence

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.

const deleteSelectedShapes = useMutation(  // You can use current "self" or "others" state in the mutation  ({ storage, self, others, setMyPresence }) => {    // Delete the selected shapes    const shapes = storage.get("shapes");    for (const shapeId of self.presence.selectedShapeIds) {      shapes.delete(shapeId);    }
// Clear the current selection setMyPresence({ selectedShapeIds: [] }); }, []);
// JSXreturn <button onClick={deleteSelectedShapes} />;

Mutations are automatically batched, so when using useMutation there’s no need to use useBatch, or call room.batch() manually.

ESLint rule

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:

{  "rules": {    // ...    "react-hooks/exhaustive-deps": ["warn", {      "additionalHooks": "useMutation"    }]  }}

Comments

useThreads

Returns the threads within the current room.

import { useThreads } from "./liveblocks.config";
const { threads, error, isLoading } = useThreads();

useThreads supports a few options as its first argument, one is query and it can be used to filter threads based on their metadata.

// Returns the threads with `metadata.color === "blue"` and `metadata.resolved === true`const { threads } = useThreads({  query: {    metadata: {      // Custom metadata query goes here      color: "blue",      resolved: true,    },  },});

Another option is scrollOnLoad, which is enabled by default. When enabled, if the URL’s hash is set to a comment ID (e.g. https://example.com/my-room#cm_xxx), the page will scroll to that comment once the threads are loaded.

👉 A Suspense version of this hook is also available, which will never return a loading state and will throw when there’s an error.

useThreadSubscription

Returns the subscription status of a thread.

const { status, unreadSince } = useThreadSubscription("th_xxx");

useCreateThread

Returns a function that creates a thread with an initial comment, and optionally some metadata.

const createThread = useCreateThread();const thread = createThread({ body: {}, metadata: {} });

useEditThreadMetadata

Returns a function that edits a thread’s metadata.

const editThreadMetadata = useEditThreadMetadata();const thread = editThreadMetadata({ threadId: "th_xxx", metadata: {} });

useMarkThreadAsRead

Returns a function that marks a thread as read.

const markThreadAsRead = useMarkThreadAsRead();markThreadAsRead("th_xxx");

useCreateComment

Returns a function that adds a comment to a thread.

const createComment = useCreateComment();const comment = createComment({ threadId: "th_xxx", body: {} });

useEditComment

Returns a function that edits a comment’s body.

const editComment = useEditComment();const comment = editComment({  threadId: "th_xxx",  commentId: "cm_xxx",  body: {},});

useDeleteComment

Returns a function that deletes a comment. If it is the last non-deleted comment, the thread also gets deleted.

const deleteComment = useDeleteComment();deleteComment({ threadId: "th_xxx", commentId: "cm_xxx" });

useAddReaction

Returns a function that adds a reaction to a comment.

const addReaction = useAddReaction();addReaction({ threadId: "th_xxx", commentId: "cm_xxx", emoji: "👍" });

useRemoveReaction

Returns a function that removes a reaction from a comment.

const removeReaction = useRemoveReaction();removeReaction({ threadId: "th_xxx", commentId: "cm_xxx", emoji: "👍" });

Notifications

useInboxNotifications

Returns the inbox notifications for the current user.

const { inboxNotifications, error, isLoading } = useInboxNotifications();

👉 A Suspense version of this hook is also available, which will never return a loading state and will throw when there’s an error.

useUnreadInboxNotificationsCount

Returns the number of unread inbox notifications for the current user.

const { count, error, isLoading } = useUnreadInboxNotificationsCount();

👉 A Suspense version of this hook is also available, which will never return a loading state and will throw when there’s an error.

useMarkInboxNotificationAsRead

Returns a function that marks an inbox notification as read.

const markInboxNotificationAsRead = useMarkInboxNotificationAsRead();markInboxNotificationAsRead("in_xxx");

useMarkAllInboxNotificationsAsRead

Returns a function that marks all inbox notifications as read.

const markAllInboxNotificationsAsRead = useMarkAllInboxNotificationsAsRead();markAllInboxNotificationsAsRead();

useRoomNotificationSettings

Returns the user’s notification settings for the current room and a function to update them.

const [{ settings }, updateSettings] = useRoomNotificationSettings();

useUpdateRoomNotificationSettings

Returns a function that updates the user’s notification settings for the current room.

Use this if you don’t need the current user’s notification settings in your component, but you need to update them (e.g. an unsubscribe button).

const updateRoomNotificationSettings = useUpdateRoomNotificationSettings();updateRoomNotificationSettings({ threads: "all" });

Miscellaneous

useUser

Returns user info from a given user ID.

import { useUser } from "./liveblocks.config";
const { user, error, isLoading } = useUser("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.

useRoomInfo

Returns room info from a given room ID.

import { useRoomInfo } from "./liveblocks.config";
const { info, error, isLoading } = useRoomInfo("room-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.

Helpers

shallow

Compares two values shallowly. This can be used as the second argument to selector based functions to loosen the equality check:

const redShapes = useStorage(  (root) => root.shapes.filter((shape) => shape.color === "red"),  shallow // 👈 here);

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):

// Comparing arraysshallow([1, 2, 3], [1, 2, 3]); // true
// Comparison objectsshallow({ a: 1 }, { a: 1 }); // true

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.

How selectors work

The concepts and behaviors described in this section apply to all of our selector hooks: useStorage , useSelf , useOthers , useOthersMapped, and useOther (singular).

Component.tsx
const child = useStorage((root) => root.child);const nested = useStorage((root) => root.child.nested);const total = useStorage((root) => root.x + root.y);const merged = useStorage((root) => [...root.items, ...root.more], shallow);

In a nutshell, the key behaviors for all selector APIs are:

Let’s go over these traits and responsibilities in the next few sections.

Selectors receive immutable data

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 root
  • useSelf((me) => ...) receives the current user
  • useOthers((others) => ...) receives a list of other users in the room
  • useOthersMapped((other) => ...) receives each individual other user in the room
  • useOther(connectionId, (other) => ...) receives a specific user in the room

For 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.

Component.tsx
function Component() {  useStorage((root) => ...);  //          ^^^^  //          Read-only. No mutable Live structures in here.  //  //          {  //            animals: ["🦁", "🦊", "🐵"],  //            mathematician: { firstName: "Ada", lastName: "Lovelace" },  //            fruitsByName: new Map([  //              ["apple", "🍎"],  //              ["banana", "🍌"],  //              ["cherry", "🍒"],  //            ])  //          }  //}

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 return arbitrary values

Component.tsx
const animals = useStorage((root) => root.animals);// ["🦁", "🦊", "🐵"]
const ada = useStorage((root) => root.mathematician);// { firstName: "Ada", lastName: "Lovelace" }
const fullname = useStorage( (root) => `${root.mathematician.firstName} ${root.mathematician.lastName}`);// "Ada Lovelace"
const fruits = useStorage((root) => [...root.fruitsByName.values()], shallow);// ["🍎", "🍌", "🍒"]

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.

Selector functions must return a stable result

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).

Examples of stable results

(root) => root.animals is stable

Liveblocks 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 stable

Because .map() creates a new array instance every time. You’ll need to use shallow here.

(root) => root.animals.map(...).join(", ") is stable

Because .join() ultimately returns a string and all primitive values are always stable.

Use a shallow comparison if the result isn’t 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:

import { shallow } from "@liveblocks/react";
// ❌ Bad - many unnecessary rerendersconst uncheckedItems = useStorage((root) => root.todos.filter((item) => !item.done));
// ✅ Greatconst uncheckedItems = useStorage( (root) => root.todos.filter((item) => !item.done), shallow // 👈 The fix!);

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 auto-subscribe to updates

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:

function Component() {  const animals = useStorage((root) => root.animals);}

And the following timeline:

  • First render, root.animals initially is ["🦁", "🦊", "🐵"].
  • Then, something unrelated elsewhere in Storage is changed. In response to the change, root.animals gets re-evaluated, but it still returns the same (unchanged) array instance.
  • Since the value didn’t change, no rerender is needed.
  • Then, someone removes an animal from the list. In response to the change, root.animals gets re-evaluated, and now it returns ["🦁", "🦊"].
  • Because the previous value and this value are different, the component will rerender, seeing the updated value.

Adopting Suspense

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:

// This example uses the "normal" version of useSelf
function MyComponent() { const cursor = useSelf((me) => me.presence.cursor);
// Liveblocks hasn’t loaded yet... if (cursor === null) { return null; }
const { x, y } = cursor; return <Cursor x={x} y={y} />;}

Into:

// This example uses the "Suspense" version of useSelf
function MyComponent() { // Will never be null, so you can directly unpack 👍 const { x, y } = useSelf((me) => me.presence.cursor); return <Cursor x={x} y={y} />;}

Importing the special Suspense hooks

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.

liveblocks.config.ts
export const {  suspense: {    RoomProvider,    useHistory,    useSelf,    useOthers,    useStorage,    /* ... */  },} = createRoomContext(client);

Now all these hooks will no longer return null values while Liveblocks is still loading, and instead suspend rendering.

Setting up the Suspense boundary

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:

import { Suspense } from "react";
function Page() { return ( <RoomProvider /* ... */> <Suspense fallback={<Loading />}> <App /> </Suspense> </RoomProvider> );}

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.

Avoid calling Suspense hooks on the server side

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.

// Drop-in replacement for standard `Suspense`import { ClientSideSuspense } from "@liveblocks/react";
function Page() { return ( <RoomProvider /* ... */> <ClientSideSuspense fallback={<Loading />}> {() => <App />} </ClientSideSuspense> </RoomProvider> );}