API Reference@liveblocks/react

@liveblocks/react provides you with React bindings for our realtime collaboration APIs, built on top of WebSockets. Read our getting started guides to learn more.

Suspense

All Liveblocks React components and hooks can be exported from two different locations, @liveblocks/react/suspense and @liveblocks/react. This is because Liveblocks provides two types of hooks; those that support React Suspense, and those that don’t.

// Import the Suspense hookimport { useThreads } from "@/liveblocks/react/suspense";
// Import the regular hookimport { useThreads } from "@/liveblocks/react";

We recommend importing from @liveblocks/react/suspense and using Suspense by default, as it often makes it easier to build your collaborative application.

Suspense hooks

Suspense hooks can be wrapped in ClientSideSuspense, which acts as a loading spinner for any components below it. When using this, all components below will only render once their hook contents have been loaded.

import { ClientSideSuspense, useStorage } from "@liveblocks/react/suspense";
function App() { <ClientSideSuspense fallback={<div>Loading…</div>}> <Component /> </ClientSideSuspense>;}
function Component() { // `animals` is always defined const animals = useStorage((root) => root.animals);
// ...}

Advanced hooks using the { ..., error, isLoading } syntax, such as useThreads, can also use ErrorBoundary to render an error if the hook runs into a problem.

import { ClientSideSuspense, useThreads } from "@liveblocks/react/suspense";import { ErrorBoundary } from "react-error-boundary";
function App() { return ( <ErrorBoundary fallback={<div>Error</div>}> <ClientSideSuspense fallback={<div>Loading…</div>}> <Component /> </ClientSideSuspense> </ErrorBoundary> );}
function Component() { // `threads` is always defined const { threads } = useThreads();
// ...}

An advantage of Suspense hooks is that you can have multiple different hooks in your tree, and you only need a single ClientSideSuspense component to render a loading spinner for all of them.

Regular hooks

Regular hooks often return null whilst a component is loading, and you must check for this to render a loading spinner.

import { useStorage } from "@liveblocks/react";
function Component() { // `animals` is `null` when loading const animals = useStorage((root) => root.animals);
if (!animals) { return <div>Loading…</div>; }
// ...}

Advanced hooks using the { ..., error, isLoading } syntax, such as useThreads, require you to make sure there isn’t a problem before using the data.

import { useThreads } from "@liveblocks/react";
function Component() { // Check for `error` and `isLoading` before `threads` is defined const { threads, error, isLoading } = useThreads();
if (error) { return <div>Error</div>; }
if (isLoading) { return <div>Loading…</div>; }
// ...}

ClientSideSuspense

Liveblocks provides a component named ClientSideSuspense which works as a replacement for Suspense. This is helpful as our Suspense hooks will throw an error when they’re run on the server, and this component avoids this issue by always rendering the fallback on the server.

import { ClientSideSuspense } from "@liveblocks/react/suspense";
function Page() { return ( <LiveblocksProvider authEndpoint="/api/liveblocks-auth"> <RoomProvider id="my-room-id"> <ClientSideSuspense fallback={<div>Loading…</div>}> <App /> </ClientSideSuspense> </RoomProvider> </LiveblocksProvider> );}

Loading spinners

Instead of wrapping your entire Liveblocks application inside a single ClientSideSuspense component, you can use multiple of these components in different parts of your application, and each will work as a loading fallback for any components further down your tree.

import { ClientSideSuspense } from "@liveblocks/react/suspense";
function Page() { return ( <LiveblocksProvider authEndpoint="/api/liveblocks-auth"> <RoomProvider id="my-room-id"> <header>My title</header>
<main> <ClientSideSuspense fallback={<div>Loading…</div>}> <Canvas /> </ClientSideSuspense> </main>
<aside> <ClientSideSuspense fallback={<div>Loading…</div>}> <LiveAvatars /> </ClientSideSuspense> </aside> </RoomProvider> </LiveblocksProvider> );}

This is a great way to build a static skeleton around your dynamic collaborative application.

Liveblocks

LiveblocksProvider

Sets up a client for connecting to Liveblocks, and is the recommended way to create to do this for React apps. You must define either authEndpoint or publicApiKey. Resolver functions should be placed inside here, and a number of other options are available, which correspond with those passed to createClient. Unlike RoomProvider, LiveblocksProvider doesn’t call Liveblocks servers when mounted, and it should be placed higher in your app’s component tree.

import { LiveblocksProvider } from "@liveblocks/react/suspense";
function App() { return ( <LiveblocksProvider // publicApiKey="" // authEndpoint="/api/liveblocks-auth" > {/* children */} </LiveblocksProvider> );}
Props
  • authEndpoint

    The URL of your back end’s authentication endpoint as a string, or an async callback function that returns a Liveblocks token result. Either authEndpoint or publicApikey are required. Learn more about using a URL string and using a callback.

  • publicApiKeystring

    The public API key taken from your project’s dashboard. Generally not recommended for production use. Either authEndpoint or publicApikey are required. Learn more.

  • throttlenumberDefault is 100

    The throttle time between WebSocket messages in milliseconds, a number between 16 and 1000 is allowed. Using 16 means your app will update 60 times per second. Learn more.

  • lostConnectionTimeoutnumberDefault is 5000

    After a user disconnects, the time in milliseconds before a "lost-connection" event is fired. Learn more.

  • backgroundKeepAliveTimeoutnumber

    The time before an inactive WebSocket connection is disconnected. This is disabled by default, but setting a number will activate it. Learn more.

  • resolveUsers

    A function that resolves user information in Comments. Return an array of TUserMeta["info"] objects in the same order they arrived. Learn more.

  • resolveRoomsInfo

    A function that resolves room information in Comments. Return an array of RoomInfo objects in the same order they arrived. Learn more.

  • resolveMentionSuggestions

    A function that resolves mention suggestions in Comments. Return an array of user IDs. Learn more.

  • polyfills

    Place polyfills for atob, fetch, and WebSocket inside here. Useful when using a non-browser environment, such as Node.js or React Native.

  • unstable_fallbackToHTTPbooleanDefault is false

    Experimental. Automatically fall back to HTTP when a message is too large for WebSockets.

  • unstable_streamDatabooleanDefault is false

    Experimental. Stream the initial Storage content over HTTP, instead of waiting for a large initial WebSocket message to be sent from the server.

LiveblocksProvider with public key

When creating a client with a public key, you don’t need to set up an authorization endpoint. We only recommend using a public key when prototyping, or on public landing pages, as it makes it possible for end users to access any room’s data. You should instead use an auth endpoint.

import { LiveblocksProvider } from "@liveblocks/react/suspense";
function App() { return ( <LiveblocksProvider publicApiKey={""}> {/* children */} </LiveblocksProvider> );}

LiveblocksProvider with auth endpoint

If you are not using a public key, you need to set up your own authEndpoint. Please refer to our Authentication guide.

import { LiveblocksProvider } from "@liveblocks/react/suspense";
function App() { return ( <LiveblocksProvider authEndpoint="/api/liveblocks-auth"> {/* children */} </LiveblocksProvider> );}

LiveblocksProvider with auth endpoint callback

If you need to add additional headers or use your own function to call your endpoint, authEndpoint can be provided as a custom callback. You should return the token created with Liveblocks.prepareSession or liveblocks.identifyUser, learn more in authentication guide.

import { LiveblocksProvider } from "@liveblocks/react/suspense";
function App() { return ( <LiveblocksProvider authEndpoint={async (room) => { // Fetch your authentication endpoint and retrieve your access or ID token // ...
return { token: "..." }; }} > {/* children */} </LiveblocksProvider> );}

room is the room ID that the user is connecting to. When using Notifications, room can be undefined, as the client is requesting a token that grants access to multiple rooms, rather than a specific room.

Fetch your endpoint

Here’s an example of fetching your API endpoint at /api/liveblocks-auth within the callback.

import { LiveblocksProvider } from "@liveblocks/react/suspense";
function App() { return ( <LiveblocksProvider authEndpoint={async (room) => { const response = await fetch("/api/liveblocks-auth", { method: "POST", headers: { Authentication: "<your own headers here>", "Content-Type": "application/json", }, // Don't forget to pass `room` down. Note that it // can be undefined when using Notifications. body: JSON.stringify({ room }), }); return await response.json(); }} > {/* children */} </LiveblocksProvider> );}
Token details

You should return the token created with Liveblocks.prepareSession or liveblocks.identifyUser. These are the values the functions can return.

  1. A valid token, it returns a { "token": "..." } shaped response.
  2. A token that explicitly forbids access, it returns an { "error": "forbidden", "reason": "..." } shaped response. If this is returned, the client will disconnect and won't keep trying to authorize.

Any other error will be treated as an unexpected error, after which the client will retry the request until it receives either 1. or 2.

WebSocket throttle

By default, the client throttles the WebSocket messages sent to one every 100 milliseconds, which translates to 10 updates per second. It’s possible to override that configuration with the throttle option with a value between 16 and 1000 milliseconds.

import { LiveblocksProvider } from "@liveblocks/react/suspense";
function App() { return ( <LiveblocksProvider throttle={16}
// Other options // ... > {/* children */} </LiveblocksProvider> );}

This option is helpful for smoothing out realtime animations in your application, as you can effectively increase the framerate without using any interpolation. Here are some examples with their approximate frames per second (FPS) values.

throttle:  16, // 60 FPSthrottle:  32, // 30 FPSthrottle: 200, //  5 FPS

Lost connection timeout

If you’re connected to a room and briefly lose connection, Liveblocks will reconnect automatically and quickly. However, if reconnecting takes longer than usual, for example if your network is offline, then the room will emit an event informing you about this.

How quickly this event is triggered can be configured with the lostConnectionTimeout setting, and it takes a number in milliseconds. lostConnectionTimeout can be set between 1000 and 30000 milliseconds. The default is 5000, or 5 seconds.

import { LiveblocksProvider } from "@liveblocks/react/suspense";
function App() { return ( <LiveblocksProvider lostConnectionTimeout={5000}
// Other props // ... > {/* children */} </LiveblocksProvider> );}

You can listen to the event with useLostConnectionListener. Note that this also affects when others are reset to an empty array after a disconnection. This helps prevent temporary flashes in your application as a user quickly disconnects and reconnects. For a demonstration of this behavior, see our connection status example.

Background keep-alive timeout

By default, Liveblocks applications will maintain an active WebSocket connection to the Liveblocks servers, even when running in a browser tab that’s in the background. However, if you’d prefer for background tabs to disconnect after a period of inactivity, then you can use backgroundKeepAliveTimeout.

When backgroundKeepAliveTimeout is specified, the client will automatically disconnect applications that have been in an unfocused background tab for at least the specified time. When the browser tab is refocused, the client will immediately reconnect to the room and synchronize the document.

import { LiveblocksProvider } from "@liveblocks/react/suspense";
function App() { return ( <LiveblocksProvider // Disconnect users after 15 minutes of inactivity backgroundKeepAliveTimeout={15 * 60 * 1000}
// Other props // ... > {/* children */} </LiveblocksProvider> );}

backgroundKeepAliveTimeout accepts a number in milliseconds—we advise using a value of at least a few minutes, to avoid unnecessary disconnections.

resolveUsers

Comments stores user IDs in its system, but no other user information. To display user information in Comments components, such as a user’s name or avatar, you need to resolve these IDs into user objects. This function receives a list of user IDs and you should return a list of user objects of the same size, in the same order.

import { LiveblocksProvider } from "@liveblocks/react/suspense";
function App() { return ( <LiveblocksProvider resolveUsers={async ({ userIds }) => { const usersData = await (userIds);
return usersData.map((userData) => ({ name: userData.name, avatar: userData.avatar.src, })); }}
// Other props // ... > {/* children */} </LiveblocksProvider> );}

The name and avatar you return are rendered in Thread components.

User objects

The user objects returned by the resolver function take the shape of UserMeta["info"], which contains name and avatar by default. These two values are optional, though if you’re using the Comments default components, they are necessary. Here’s an example of userIds and the exact values returned.

import { LiveblocksProvider } from "@liveblocks/react/suspense";
function App() { return ( <LiveblocksProvider resolveUsers={async ({ userIds }) => { // ["marc@example.com", "nimesh@example.com"]; console.log(userIds);
return [ { name: "Marc", avatar: "https://example.com/marc.png" }, { name: "Nimesh", avatar: "https://example.com/nimesh.png" }, ]; }}
// Other props // ... > {/* children */} </LiveblocksProvider> );}

You can also return custom information, for example, a user’s color:

import { LiveblocksProvider } from "@liveblocks/react/suspense";
function App() { return ( <LiveblocksProvider resolveUsers={async ({ userIds }) => { // ["marc@example.com"]; console.log(userIds);
return [ { name: "Marc", avatar: "https://example.com/marc.png", color: "purple", }, ]; }}
// Other props // ... > {/* children */} </LiveblocksProvider> );}
Accessing user data

You can access any values set within resolveUsers with the useUser hook.

import { useUser } from "@liveblocks/react/suspense";
function Component() { const user = useUser("marc@example.com");
// { name: "Marc", avatar: "https://...", ... } console.log(user);}

resolveRoomsInfo

When using Notifications with Comments, room IDs will be used to contextualize notifications (e.g. “Chris mentioned you in room-id”) in the InboxNotification component. To replace room IDs with more fitting names (e.g. document names, “Chris mentioned you in Document A”), you can provide a resolver function to the resolveRoomsInfo option in LiveblocksProvider.

This resolver function will receive a list of room IDs and should return a list of room info objects of the same size and in the same order.

import { LiveblocksProvider } from "@liveblocks/react/suspense";
function App() { return ( <LiveblocksProvider resolveRoomsInfo={async ({ roomIds }) => { const documentsData = await (roomIds);
return documentsData.map((documentData) => ({ name: documentData.name, // url: documentData.url, })); }}
// Other props // ... > {/* children */} </LiveblocksProvider> );}

In addition to the room’s name, you can also provide a room’s URL as the url property. If you do so, the InboxNotification component will automatically use it. It’s possible to use an inbox notification’s roomId property to construct a room’s URL directly in React and set it on InboxNotification via href, but the room ID might not be enough for you to construct the URL , you might need to call your backend for example. In that case, providing it via resolveRoomsInfo is the preferred way.

resolveMentionSuggestions

To enable creating mentions in Comments, you can provide a resolver function to the resolveMentionSuggestions option in LiveblocksProvider. These mentions will be displayed in the Composer component.

This resolver function will receive the mention currently being typed (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.

import { LiveblocksProvider } from "@liveblocks/react/suspense";
function App() { return ( <LiveblocksProvider resolveMentionSuggestions={async ({ text, roomId }) => { const workspaceUsers = await (roomId);
if (!text) { // Show all workspace users by default return (workspaceUsers); } else { const matchingUsers = (workspaceUsers, text); return (matchingUsers); } }}
// Other props // ... > {/* children */} </LiveblocksProvider> );}

LiveblocksProvider for Node.js

To use @liveblocks/client in Node.js, you need to provide WebSocket and fetch polyfills. As polyfills, we recommend installing ws and node-fetch.

$npm install ws node-fetch

Then, pass them to the LiveblocksProvider polyfill option as below.

import { LiveblocksProvider } from "@liveblocks/react/suspense";import fetch from "node-fetch";import WebSocket from "ws";
function App() { return ( <LiveblocksProvider polyfills={{ fetch, WebSocket, }}
// Other props // ... > {/* children */} </LiveblocksProvider> );}

Note that node-fetch v3+ does not support CommonJS. If you are using CommonJS, downgrade node-fetch to v2.

LiveblocksProvider for React Native

To use @liveblocks/client with React Native, you need to add an atob polyfill. As a polyfill, we recommend installing base-64.

$npm install base-64

Then you can pass the decode function to our atob polyfill option when you create the client.

import { LiveblocksProvider } from "@liveblocks/react/suspense";import { decode } from "base-64";
function App() { return ( <LiveblocksProvider polyfills={{ atob: decode, }}
// Other props // ... > {/* children */} </LiveblocksProvider> );}

createLiveblocksContext

Creates a LiveblocksProvider and a set of typed hooks. Note that any LiveblocksProvider created in this way takes no props, because it uses settings from the client instead. 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,
// Other hooks // ...} = createLiveblocksContext(client);

useClient

Returns the client of the nearest LiveblocksProvider above in the React component tree.

import { useClient } from "@liveblocks/react/suspense";
const client = useClient();

Room

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. When using Realtime APIs, initial Presence values for each user, and Storage values for the room can be set.

import { RoomProvider } from "@liveblocks/react/suspense";
function App() { return <RoomProvider id="my-room-id">{/* children */}</RoomProvider>;}
Props
  • idstringRequired

    The unique ID for the current room. RoomProvider will join this room when it loads. If the room doesn’t exist already it will automatically create the room first then join. After setting up authentication for your app, it can helpful to decide on a naming pattern for your room IDs.

  • initialPresenceJsonObject

    The initial Presence of the user entering the room. Each user has their own presence, and this is readable for all other connected users. A user’s Presence resets every time they disconnect. This object must be JSON-serializable. This value is ignored after the first render. Learn more.

  • initialStorageLsonObject

    The initial Storage structure for the room when it’s joined for the first time. This is only set a single time, when the room has not yet been populated. This object must contain conflict-free live structures. This value is ignored after the first render, and if Storage for the current room has already been created. Learn more.

  • autoConnectbooleanDefault is true

    Whether the room immediately connects to Liveblocks servers. This value is ignored after the first render.

Setting initial Presence

Presence is used for storing temporary user-based values, such as a user’s cursor coordinates, or their current selection. Each user has their own presence, and this is readable for all other connected users. Set your initial Presence value by using initialPresence.

import { RoomProvider } from "@liveblocks/react/suspense";
function App() { return ( <RoomProvider id="my-room" initialPresence={{ cursor: null, colors: ["red", "purple"], selection: { id: 72426, }, }} > {/* children */} </RoomProvider> );}

Each user’s Presence resets every time they disconnect, as this is only meant for temporary data. Any JSON-serializable object is allowed (the JsonObject type).

Setting initial Storage

Storage is used to store permanent data that’s used in your application, such as shapes on a whiteboard, nodes on a flowchart, or text in a form. The first time a room is entered, you can set an initial value by using initialStorage. Note that this value is only read a single time.

import { LiveList, LiveObject, LiveMap } from "@liveblocks/client";import { RoomProvider } from "@liveblocks/react/suspense";
function App() { return ( <RoomProvider id="my-room" initialPresence={} initialStorage={{ title: "Untitled", names: new LiveList(["Steven", "Guillaume"]), shapes: new LiveMap([ ["g9shu0", new LiveObject({ type: "rectangle", color: "red" })], ["djs3g5", new LiveObject({ type: "circle", color: "yellow" })], ]), }} > {/* children */} </RoomProvider> );}

Any conflict-free live structures and JSON-serializable objects are allowed (the LsonObject type).

createRoomContext

Creates a RoomProvider and a set of typed hooks to use in your app. Note that any RoomProvider created in this way does not need to be nested in LiveblocksProvider, as it already has access to the client. We generally recommend typing your app using the newer method instead. When using createRoomContext it can be helpful to use it in liveblocks.config.ts and re-export your typed hooks as below.

liveblocks.config.ts
import { createClient } from "@liveblocks/client";import { createRoomContext } from "@liveblocks/react";
const client = createClient({ // publicApiKey: "", // authEndpoint: "/api/liveblocks-auth",});
type Presence = {};type Storage = {};type UserMeta = {};type RoomEvent = {};type ThreadMetadata = {};
export const { RoomProvider, useMyPresence,
// Other hooks // ...} = createRoomContext<Presence, Storage, UserMeta, RoomEvent, ThreadMetadata>( client);

Suspense with createRoomContext

To use the React suspense version of our hooks with createRoomContext, you can export from the suspense property instead.

liveblocks.config.ts
import { createClient } from "@liveblocks/client";import { createRoomContext } from "@liveblocks/react";
const client = createClient({ // publicApiKey: "", // authEndpoint: "/api/liveblocks-auth",});
type Presence = {};type Storage = {};type UserMeta = {};type RoomEvent = {};type ThreadMetadata = {};
export const { suspense: { RoomProvider, useMyPresence,
// Other suspense hooks // ... },} = createRoomContext<Presence, Storage, UserMeta, RoomEvent, ThreadMetadata>( client);

Typing createRoomContext

To type your hooks, you can pass multiple different types to createRoomContext. A full explanation is in the code snippet below.

useRoom

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

import { useRoom } from "@liveblocks/react/suspense";
const room = useRoom();

useIsInsideRoom

Returns a boolean, true if the hook was called inside a RoomProvider context, and false otherwise.

import { useIsInsideRoom } from "@liveblocks/react/suspense";
const isInsideRoom = useIsInsideRoom();

Displaying different components inside rooms

useIsInsideRoom is helpful for rendering different components depending on whether they’re inside a room, or not. One example is a header component that only displays a live avatar stack when users are connected to the room.

import { useIsInsideRoom, useOthers } from "@liveblocks/react/suspense";
function Header() { const isInsideRoom = useIsInsideRoom();
return ( <div> {isInsideRoom ? <LiveAvatars /> : null} <MyAvatar /> </div> );}
function LiveAvatars() { const others = useOthers(); return others.map((other) => <img src={other.info.picture} />);}

Here’s how the example above would render in three different LiveblocksProvider and RoomProvider contexts.

// 👥👤 Live avatar stack and your avatar<LiveblocksProvider /* ... */>  <RoomProvider /* ... */>    <Header />  </RoomProvider></LiveblocksProvider>

// 👤 Just your avatar<LiveblocksProvider /* ... */> <Header /></LiveblocksProvider>
// 👤 Just your avatar<Header />

useErrorListener

Listen to potential room connection errors.

import { useErrorListener } from "@liveblocks/react/suspense";
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/react/suspense";
const status = useStatus();

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

useStorageStatus

Returns the current storage status of the room, and will re-render your component whenever it changes. A { smooth: true } option is also available, which prevents quick changes between states, making it ideal for rendering a synchronization badge in your app.

import { useStorageStatus } from "@liveblocks/react";
const storageStatus = useStorageStatus();// "not-loaded" | "loading" | "synchronizing" | "synchronized"

👉 A Suspense version of this hook is also available.

import { useStorageStatus } from "@liveblocks/react/suspense";
const storageStatus = useStorageStatus();// "synchronizing" | "synchronized"

Display a synchronization badge

Passing { smooth: true } prevents the status changing from "synchronizing" to "synchronized" until 1 second has passed after the final change. This means it’s ideal for rendering a Storage status badge, as it won’t flicker in a distracting manner when changes are made in quick succession.

import { useStorageStatus } from "@liveblocks/react/suspense";
function StorageStatusBadge() { const storageStatus = useStorageStatus({ smooth: true });
return ( <div>{storageStatus === "synchronized" ? "✅ Saved" : "🔄 Saving"}</div> );}

Prevent users losing unsaved changes

Storage usually synchronizes milliseconds after a change, but in some circumstances, such as a user with a poor connection, it may take longer before they receive a response from our servers. It can be helpful to check if Storage changes have been synchronized, and prevent users from leaving the page if they haven’t.

It’s possible to implement this with useStorageStatus, however with useRoom you can create a version that renders much less frequently, so we recommend using this instead.

import { useRoom } from "@liveblocks/react/suspense";
function usePreventUnsavedChanges() { const room = useRoom();
useEffect(() => { function beforeUnload(e: BeforeUnloadEvent) { if (room.getStorageStatus() === "synchronized") { return; } e.preventDefault(); }
window.addEventListener("beforeunload", beforeUnload);
return () => { window.removeEventListener("beforeunload", beforeUnload); }; }, [room]);}

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.

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/react/suspense";
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/react/suspense";
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/react/suspense";
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/react/suspense";
// 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/react/suspense";
// 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

Each room contains Storage, a conflict-free data store that multiple users can edit at the same time. When users make edits simultaneously, conflicts are resolved automatically, and each user will see the same state. Storage is ideal for storing permanent document state, such as shapes on a canvas, notes on a whiteboard, or cells in a spreadsheet.

Data structures

Storage provides three different conflict-free data structures, which you can use to build your application. All structures are permanent and persist when all users have left the room, unlike Presence which is temporary.

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

Typing Storage

To type the Storage values you receive, make sure to set your Storage type.

import { LiveList } from "@liveblocks/client";
declare global { interface Liveblocks { Storage: { animals: LiveList<string>; }; }}