We use cookies to collect data to improve your experience on our site. Read our Privacy Policy to learn more.
DeclineAccept
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.
Enable Suspense with createRoomContext
If you’re using the non-standard createRoomContext
function to build your hooks, you must
enable suspense differently.
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 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> );}
All LiveblocksProvider props
Props
- authEndpointSee full type
The URL of your back end’s authentication endpoint
as a string, or an async callback function that returns a Liveblocks token
result. The result is cached by the Liveblocks client, and called fresh only
when necessary, so never cache it yourself. 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 is100
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.
- preventUnsavedChangesbooleanDefault isfalse
When set, navigating away from the current page is prevented while Liveblocks is still synchronizing local changes. Learn\ more.
- lostConnectionTimeoutnumberDefault is5000
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.
- resolveUsersSee full type
A function that resolves user information in
Comments, Text\
Editor, and
Notifications. Return an array of
UserMeta["info"] objects in the same order they arrived. Learn\
more.
- resolveRoomsInfoSee full type
A function that resolves room information in
Notifications. Return an array of
RoomInfo objects in the same order they arrived. Learn\
more.
- resolveGroupsInfoSee full type
A function that resolves group information in
Comments and Text\
Editor. Return an array of
GroupInfo objects in the same order they arrived. Learn\
more.
- resolveMentionSuggestionsSee full type
A function that resolves mention suggestions in Comments and Text\ Editor. Return an array of user IDs or mention objects. 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.
- badgeLocationDefault is"bottom-right"
The location of the "Powered by Liveblocks" badge. Can be set to either
"top-right", "bottom-right", "bottom-left", or "top-left". Learn\
more.
- unstable_streamDatabooleanDefault isfalseDeprecated
Deprecated. For new rooms, use engine: 2 instead. Engine
2 rooms have native support for streaming. This flag will be removed in a
future version, but will continue to work for existing engine 1 rooms for
now. Learn more.
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.
Sign in to get your own API keys.
import { LiveblocksProvider } from "@liveblocks/react/suspense";
function App() { return ( <LiveblocksProvider publicApiKey={"pk_prod_xxxxxxxxxx…xxxxxxxxpk_prod_xxxxxxxxxxxxxxxxxxxxxxxx"}> {/* 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> );}
Don't cache tokens
Never cache your authentication endpoint, as your client will not function correctly. The Liveblocks client will cache results for you, only making requests to the endpoint if necessary, such as when the token has expired.
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.
- A valid token, it returns a
{ "token": "..." }shaped response. - 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
Prevent users losing unsaved changes
Liveblocks usually synchronizes milliseconds after a local change, but if a user
immediately closes their tab, or if they have a slow connection, it may take
longer for changes to synchronize. Enabling preventUnsavedChanges will stop
tabs with unsaved changes closing, by opening a dialog that warns users. In
usual circumstances, it will very rarely trigger.
function Page() { return ( <LiveblocksProvider preventUnsavedChanges
// Other options // ... > ... </LiveblocksProvider> );}
More specifically, this option triggers when:
- There are unsaved changes after calling any hooks or methods, in all of our products.
- There are unsaved changes in a Text Editor.
- There’s an unsubmitted comment in the Composer.
- The user has made changes and is currently offline.
Internally, this option uses the beforeunload event.
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 and Text Editor store user IDs in its system, but no other user information. To display user information in Comments, Text Editor, and Notifications 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.
User IDs are automatically resolved in batches with a maximum of 50 users per batch to optimize performance and prevent overwhelming your user resolution function.
import { LiveblocksProvider } from "@liveblocks/react/suspense";
function App() { return ( <LiveblocksProvider resolveUsers={async ({ userIds }) => { const usersData = await __getUsersFromDB__(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 __getDocumentsFromDB__(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.
resolveGroupsInfo
When using group mentions with Comments
and Text Editor, group IDs will be used
instead of user IDs. Similarly to
resolveUsers, you can provide a resolver
function to the resolveGroupsInfo option in
LiveblocksProvider to assign information like names and
avatars to group IDs.
import { LiveblocksProvider } from "@liveblocks/react/suspense";
function App() { return ( <LiveblocksProvider resolveGroupsInfo={async ({ groupIds }) => { const groupsData = await __getGroupsFromDB__(groupIds);
return groupsData.map((groupData) => ({ name: groupData.name, avatar: groupData.avatar.src, // description: groupData.description, })); }}
// Other props // ... > {/* children */} </LiveblocksProvider> );}
Accessing group info
You can access any values set within resolveGroupsInfo with the
useGroupInfo hook.
import { useGroupInfo } from "@liveblocks/react/suspense";
function Component() { const group = useGroupInfo("group-engineering");
// { name: "Engineering", avatar: "https://...", ... } console.log(group);}
resolveMentionSuggestions
To enable creating mentions in Comments
and Text Editor, you can provide a
resolver function to the resolveMentionSuggestions option in
LiveblocksProvider. These mentions will be displayed in
the Composer component and
in text editors.
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 __getWorkspaceUsersFromDB__(roomId);
if (!text) { // Show all workspace users by default return __getUserIds__(workspaceUsers); } else { const matchingUsers = __findUsers__(workspaceUsers, text); return __getUserIds__(matchingUsers); } }}
// Other props // ... > {/* children */} </LiveblocksProvider> );}
Group mentions
To support group mentions in Comments and Text Editor, you can return a list of mention objects instead of user IDs to suggest a mix of user and group mentions.
import { LiveblocksProvider } from "@liveblocks/react/suspense";
function App() { return ( <LiveblocksProvider resolveMentionSuggestions={async ({ text, roomId }) => { const dbUsers = await __findUsersFromDB__(roomId); const dbGroups = await __findGroupsFromDB__(roomId);
// Show groups and users matching the text being typed return [ ...dbGroups.map((group) => ({ kind: "group", id: group.id, })), ...dbUsers.map((user) => ({ kind: "user", id: user.id, })), ]; }}
// Other props // ... > {/* children */} </LiveblocksProvider> );}
The mention objects specify which kind of mention it is, the ID to mention (user ID or group ID), etc.
// A user mention suggestion{ kind: "user", id: "user-1",}
// A group mention suggestion{ kind: "group", id: "group-1",}
// A group mention suggestion with fixed group members// When using fixed group members via `userIds`, they will take precedence// if the group ID exists on Liveblocks.{ kind: "group", id: "here", userIds: ["user-1", "user-2"],}
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.
Terminal
npm
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.
Terminal
npm
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> );}
Powered by Liveblocks branding
By default, Liveblocks displays a "Powered by Liveblocks" badge in your
application. You can adjust the position of the badge by setting the
badgeLocation property on LiveblocksProvider.
Set badge location
// "top-right", "bottom-right", "bottom-left", "top-left"<LiveblocksProvider badgeLocation="bottom-right"> <App /></LiveblocksProvider>
If you wish to remove remove the badge entirely, you can do so by following these steps:
- In the Liveblocks dashboard, navigate to your team’s settings.
- Under General, toggle on the remove "Powered by Liveblocks" branding option.
Removing the "Powered by Liveblocks" badge
Removing the "Powered by Liveblocks" badge on your projects requires a paid plan. See the pricing page for more information.
createLiveblocksContext
Not recommended
This used to be the default way to start your app, but now it’s recommended for
advanced usage only. We generally recommend using LiveblocksProvider and
following typing your data with the Liveblocks interface,
unless you need to define multiple room types in your application.
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);
useClientLiveblocksProvider
Returns the client of
the nearest LiveblocksProvider above in the React component tree.
import { useClient } from "@liveblocks/react/suspense";
const client = useClient();
Arguments
None
Returns
- clientClient
The Liveblocks client
instance from the nearest LiveblocksProvider.
useErrorListenerLiveblocksProvider
Listen to potential Liveblocks errors. Examples of errors include room
connection errors, errors creating threads, and errors deleting notifications.
Each error has a message string, and a context object which has different
values for each error type. context always contains an error type and
roomId.
import { useErrorListener } from "@liveblocks/react/suspense";
useErrorListener((error) => { // { message: "You don't have access to this room", context: { ... }} console.error(error);});
There are many different errors, and each can be handled separately by checking
the value of error.context.type. Below we’ve listed each error and the context
it provides.
All error types
Arguments
- callback(error: LiveblocksError) => void
A callback function that will be called when a Liveblocks error occurs. The error object contains a message and context with error-specific information.
AI Copilots
useAiChatsLiveblocksProvider
Returns a paginated list of AI chats created by the current user. Initially fetches the latest 50 chats. Suspense and regular versions of this hook are available.
import { useAiChats } from "@liveblocks/react";
const { chats, error, isLoading } = useAiChats();
Options
- queryAiChatsQuery
Optional query to filter chats by metadata values or absence of metadata keys. Learn more
Returns
- chatsAiChat[] | undefined
An array of AI chats created by the current user.
- isLoadingboolean
Whether the chats are currently being loaded.
- errorError | undefined
Any error that occurred while loading the chats. Learn\ more.
- hasFetchedAllboolean
Whether all available chats have been fetched. Learn\ more.
- fetchMore() => void
A function to fetch more chats. Learn\ more.
- isFetchingMoreboolean
Whether more chats are currently being fetched. Learn\ more.
- fetchMoreErrorError | undefined
Any error that occurred while fetching more chats. Learn\ more.
List the user's chats and switch between them
You can use the AiChat
component alongside the hook to create an AI chat switcher. Below, each button
displays the chat's automatically generated title, and chats can be deleted with
useDeleteAiChat.
import { useState } from "react";import { AiChat } from "@liveblocks/react-ui";import { useAiChats } from "@liveblocks/react";
function Chats() { const { chats, error, isLoading } = useAiChats(); const [chatId, setChatId] = useState(); const deleteChat = useDeleteAiChat();
if (isLoading) { return <div>Loading...</div>; }
if (error) { return <div>Error: {error.message}</div>; }
return ( <div style={{ display: "flex" }}> <ul> {chats.map((chat) => ( <li key={chat.id}> <button onClick={() => setChatId(chat.id)}> {chat.title || "Untitled"} </button> <button onClick={() => deleteChat(chat.id)}>❌</button> </li> ))} </ul> <AiChat chatId={chatId} /> </div> );}
Querying chats
It’s possible to return chats that match a certain query with the query
option. You can filter by metadata values, or by the absence of a metadata key.
Returned chats must match the entire query.
import { useAiChats } from "@liveblocks/react";
// Filter by metadata values and by absence of a keyconst { chats } = useAiChats({ query: { metadata: { // Match chats that are of type 'temporary' type: "temporary",
// Match chats that have all of these tags tag: ["urgent", "billing"],
// Match chats where the "archived" key does not exist archived: null, }, },});
Pagination
By default, the useAiChats hook returns up to 50 chats. To fetch more, the
hook provides additional fields for pagination, similar to useThreads.
import { useAiChats } from "@liveblocks/react";
const { chats, isLoading, error,
hasFetchedAll, fetchMore, isFetchingMore, fetchMoreError,} = useAiChats();
hasFetchedAllindicates whether all available AI chats have been fetched.fetchMoreloads up to 50 more AI chats, and is always safe to call.isFetchingMoreindicates whether more AI chats are being fetched.fetchMoreErrorreturns error statuses resulting from fetching more.
Pagination example
The following example demonstrates how to use the fetchMore function to
implement a “Load More” button, which fetches additional AI chats when clicked.
The button is disabled while fetching is in progress.
import { AiChat } from "@liveblocks/react-ui";import { useAiChats } from "@liveblocks/react";
function Inbox() { const { chats, hasFetchedAll, fetchMore, isFetchingMore } = useAiChats();
return ( <div> {chats.map((chat) => ( <AiChat key={chat.id} chatId={chat.id} /> ))} {hasFetchedAll ? ( <div>🎉 All chats loaded!</div> ) : ( <button disabled={isFetchingMore} onClick={fetchMore}> Load more </button> )} </div> );}
Error handling
Error handling is another important aspect to consider when using the
useAiChats hook. The error and fetchMoreError fields provide information
about any errors that occurred during the initial fetch or subsequent fetch
operations, respectively. You can use these fields to display appropriate error
messages to the user and implement retry mechanisms if needed.
The following example shows how to display error messages for both initial loading errors and errors that occur when fetching more inbox notifications.
import { AiChat } from "@liveblocks/react-ui";import { useAiChats } from "@liveblocks/react";
function Inbox() { const { chats, error, fetchMore, fetchMoreError } = useAiChats();
// Handle error if the initial load failed. // The `error` field is not returned by the Suspense hook as the error is thrown to nearest ErrorBoundary if (error) { return ( <div> <p>Error loading AI chats: {error.message}</p> </div> ); }
return ( <div> {chats.map((chat) => ( <AiChat key={chat.id} chatId={chat.id} /> ))}
{fetchMoreError && ( <div> <p>Error loading more AI chats: {fetchMoreError.message}</p> <button onClick={fetchMore}>Retry</button> </div> )} </div> );}
useAiChat
Returns information about an AI chat, for example its title and metadata. Titles are automatically generated from the content of the first user message in a chat, and the AI’s response. Suspense and regular versions of this hook are available.
import { useAiChat } from "@liveblocks/react";
const { chat, error, isLoading } = useAiChat("my-chat-id");
Arguments
- chatIdstring
The ID of the AI chat to retrieve information for.
Returns
- chatAiChat | undefined
The AI chat object containing title, metadata, and other properties.
- isLoadingboolean
Whether the chat information is currently being loaded.
- errorError | undefined
Any error that occurred while loading the chat information.
Displaying a default title
If chat.title is undefined after an isLoading check, that means the title
has not been set yet. You can display a default title in this case, and the
title will be displayed once generated.
import { useAiChat } from "@liveblocks/react";
function ChatTitle() { const { chat, error, isLoading } = useAiChat("my-chat-id");
if (isLoading || error) { return null; }
return <div>{chat.title || "Untitled chat"}</div>;}
useCreateAiChat
Returns a function that creates an AI chat.
import { useCreateAiChat } from "@liveblocks/react/suspense";
const createAiChat = useCreateAiChat();createAiChat("my-ai-chat");
Arguments
None
Returns
- createAiChat(chatIdOrOptions: string | CreateAiChatOptions) => AiChat
A function that creates an AI chat. Can be called with either a string ID or
an options object containing id, optional title, and optional
metadata.
Create a chat with a custom title and metadata
You can optionally set a title with useCreateAiChat, which prevents the AI
auto-generating a title from the first messages. Additionally, you can choose to
set custom metadata for the chat, strings or arrays of strings.
import { useCreateAiChat } from "@liveblocks/react/suspense";
const createAiChat = useCreateAiChat();createAiChat({ id: "my-ai-chat", title: "My AI Chat", metadata: { color: "red", tags: ["product", "engineering"], },});
useDeleteAiChat
Returns a function that deletes an AI chat by its ID. Use in conjunction with
useAiChats to
loop through each chat
and add a delete button.
import { useDeleteAiChat } from "@liveblocks/react/suspense";
const deleteAiChat = useDeleteAiChat();deleteAiChat("my-chat-id");
Arguments
None
Returns
- deleteAiChat(chatId: string) => void
A function that deletes an AI chat by its ID.
useSendAiMessage
Returns a function that sends a message to an AI chat, identified by its ID. Useful for creating suggestions in empty inside chats and sending messages on behalf of the user.
import { useSendAiMessage } from "@liveblocks/react";
const sendAiMessage = useSendAiMessage("my-chat-id");sendAiMessage("Hello!");
Remember to set your copilot ID otherwise the default copilot will be used.
const sendAiMessage = useSendAiMessage("my-chat-id", { copilotId: "co_h7GBa3...",});
Arguments
- chatIdstring
Optional. The ID of the AI chat to send messages to. Can also be provided when calling the returned function.
- optionsobject
Optional configuration object.
- options.copilotIdstring
Optional. The ID of the copilot to use for sending the message.
Returns
- sendAiMessage(messageOrOptions: string | SendAiMessageOptions) => AiChatMessage
A function that sends a message to an AI chat. Can be called with either a
string message or an options object containing text, optional chatId,
and optional copilotId.
Setting options when sending a message
Optionally you can set options when sending a message, instead of when creating the hook. Alternatively, you can also override or complete the hook’s options when calling the function by passing an object to it.
import { useSendAiMessage } from "@liveblocks/react";
// Setting a `chatId` and `copilotId`const sendAiMessage = useSendAiMessage("my-chat-id", { copilotId: "co_shSm8f...",});
// Sends to initial `chatId` and `copilotId`sendAiMessage("Hello!");
// Overwrites the `copilotId` just for this messagesendAiMessage({ text: "Hello world", copilotId: "co_Xpksa9...",});
// Overwrites the `chatId` just for this messagesendAiMessage({ text: "Hello world", chatId: "my-other-chat-id",});
You can even skip setting the chatId and copilotId in the hook, and just
pass them in the function.
const sendAiMessage = useSendAiMessage();
sendAiMessage({ text: "Hello world", chatId: "my-other-chat-id",});
Get the created message object
If necessary, you can also access the newly created message object.
import { useSendAiMessage } from "@liveblocks/react";
const sendAiMessage = useSendAiMessage("my-chat-id");const message = sendAiMessage("Hello world");
// { id: "ms_gw1wEn...", chatId: "my-chat-id", content: [...], ...}console.log(message);
useAiChatMessages
Returns a list of every message in an AI chat, identified by its ID. Updates in realtime using WebSockets.
import { useAiChatMessages } from "@liveblocks/react";
const { messages, error, isLoading } = useAiChatMessages("my-chat-id");
Arguments
- chatIdstring
The ID of the AI chat to retrieve messages from.
Returns
- messagesAiChatMessage[] | undefined
An array of messages in the AI chat.
Example messages
- isLoadingboolean
Whether the messages are currently being loaded.
- errorError | undefined
Any error that occurred while loading the messages.
useAiChatStatus
Returns the status of an AI chat, indicating whether it’s disconnected, loading, idle or actively generating content. This is a convenience hook that derives its state from the latest assistant message in the chat.
import { useAiChatStatus } from "@liveblocks/react";
const { status, partType, toolName } = useAiChatStatus("my-chat-id");
Arguments
- chatIdstring
The ID of the AI chat.
Returns
- status"disconnected" | "loading" | "idle" | "generating"
The current synchronization status of the chat. When disconnected the AI service is not available.
- partType"text" | "tool-invocation"
The type of content being generated.
- toolNamestring | undefined
The name of the tool being invoked. If no tool is currently being called,
returns undefined.
RegisterAiKnowledge
Adds knowledge to all AI features on the page. AI will understand the information you pass, and will answer questions or call tools based on it. This is particularly helpful for passing user info, app state, and other small contextual knowledge.
<RegisterAiKnowledge description="The current user's payment plan" value="Enterprise"/>
Each knowledge source has a description string, and a value which can be
either a string, object, array or JSON-serializable value that provides
meaningful context for your use case. The AI uses this data, along with the
accompanying description, to better understand and respond to user queries or
perform actions based on the supplied context. These components can be placed
anywhere in your app, so long as they’re under
LiveblocksProvider.
import { AiChat } from "@liveblocks/react-ui";import { RegisterAiKnowledge } from "@liveblocks/react";
function Chat() { return ( <> <RegisterAiKnowledge description="The current user's payment plan" value="Enterprise" /> <RegisterAiKnowledge description="The current user's info" value={{ name: "Jody Hekla", email: "jody@liveblocks.io", teams: ["Engineering", "Product"], }} /> <AiChat chatId="my-chat-id" /> </> );}
Pass in assorted context
Passing the AI context about the current datetime, the user’s language, the page the user’s visiting, and navigable pages on your website, is an effective method for improving your chat’s replies.
import { RegisterAiKnowledge } from "@liveblocks/react";
function Chat() { return ( <> <RegisterAiKnowledge description="The current date and time" value={new Date()} /> <RegisterAiKnowledge description="The user's preferred language" value={navigator.language} /> <RegisterAiKnowledge description="The current URL" value={window.location.href} /> <RegisterAiKnowledge description="URLs on this website" value={["/dashboard", "/billing", "/settings"]} /> </> );}
When building your app, it’s worth considering which app-specific context will be helpful, for example the user’s payment plan, or a list of their projects.
Pass in user data from your auth provider
You can pass in knowledge from your auth provider, for example with
useUser from
Clerk. You can tell AI that the state is loading in a
simple string, and it will understand.
import { RegisterAiKnowledge } from "@liveblocks/react";import { useUser } from "@clerk/clerk-react";
function Chat() { const { isSignedIn, user, isLoaded } = useUser();
return ( <RegisterAiKnowledge description="The current user's info" value={isLoaded ? (isSignedIn ? user : "Not signed in") : "Loading..."} /> );}
Pass in assorted data from fetching hooks
You can pass in knowledge from data fetching hooks such as with
useSWR from
SWR. You can tell AI that the state is loading in a
simple string, and it will understand.
import { RegisterAiKnowledge } from "@liveblocks/react";import useSWR from "swr";
function Chat() { const { data, error, isLoading } = useSWR(`/important-data`, fetcher);
return ( <RegisterAiKnowledge description="Important data" value={isLoading ? "Loading..." : error ? "Problem fetching data" : data} /> );}
const fetcher = (...args) => fetch(...args).then((res) => res.json());
Pass in text editor document data
You can pass in knowledge from your text editor, for example when using Liveblocks Tiptap.
import { RegisterAiKnowledge } from "@liveblocks/react-ui";import { useLiveblocksExtension } from "@liveblocks/react-tiptap";import { useEditor, EditorContent } from "@tiptap/react";
function TextEditor() { const liveblocks = useLiveblocksExtension();
const editor = useEditor({ extensions: [ liveblocks, // ... ], });
return ( <div> <RegisterAiKnowledge description="The current document's Tiptap state" value={editor.getHTML()} /> <EditorContent editor={editor} /> </div> );}
As well as editor.getHTML(), editor.getJSON() and editor.getText() are
also available when using Tiptap. Its worth trying them all, in case your AI
model understands one of them better than the others.
Pass in comment data
You can also pass in knowledge from your custom Liveblocks Comments app with
useThreads. This way, your
AI chat will understand the context in the current room.
import { RegisterAiKnowledge, useThreads } from "@liveblocks/react";
function Comments() { const { threads, isLoading, error } = useThreads();
return ( <div> <RegisterAiKnowledge description="The comment threads in the current document" value={ isLoading ? "Loading..." : error ? "Problem fetching threads" : threads } /> {threads.map((thread) => ( <Thread key={thread.id} thread={thread} /> )} </div> );}
Pass in storage data
You can also pass in knowledge from your custom Liveblocks storage app with
useStorage. This way, your
AI chat will understand the context in the current room.
import { RegisterAiKnowledge, useStorage } from "@liveblocks/react/suspense";
function Whiteboard() { const shapes = useStorage((root) => root.shapes);
return ( <div> <RegisterAiKnowledge description="The current shapes on the whiteboard" value={shapes} /> {shapes.map((shape) => ( // ... )} </div> );}
Props
- descriptionstringRequired
A clear description of what this knowledge represents. This helps the AI understand the context and relevance of the provided information.
- valueJsonRequired
The actual data or information to share with the AI. Can be a string, object, array, or any JSON-serializable information that offers context relevant to your application or user.
- idstring
Optional unique identifier for this knowledge source. If provided, subsequent updates with the same ID will replace the previous knowledge.
- chatIdstring
Optional chat ID to scope this tool to a specific chat. If provided, the tool will only be available to that chat.
RegisterAiTool
Registers a tool that can be used by AI chats on the page. Tools allow AI to autonomously run actions, render custom components, and show confirmation or human-in-the-loop UIs within the chat.
import { RegisterAiTool } from "@liveblocks/react";import { defineAiTool } from "@liveblocks/client";
<RegisterAiTool name="my-tool" tool={defineAiTool()({ // Tool definition // ... })}/>;
defineAiTool is used to
create a tool definition, and you can supply parameters as a
JSON Schema that the AI can fill in. If you supply
an execute function the AI will call it. render is used to show UI inside
the chat. Below is an example of a tool that lets AI get the current weather in
a given location, then renders a component in the chat.
function App() { return ( <> <RegisterAiTool name="get-weather" tool={defineAiTool()({ description: "Get current weather information", parameters: { type: "object", properties: { location: { type: "string", description: "City name" }, }, required: ["location"], additionalProperties: false, }, execute: async (args) => { const { temperature, condition } = await __getWeather__( args.location ); return { data: { temperature, condition } }; }, render: ({ result }) => ( <AiTool title="Weather Lookup" icon="🌤️"> {result.data ? ( <div> {result.data.temperature}°F - {result.data.condition} </div> ) : null} </AiTool> ), })} /> <AiChat chatId="my-chat" /> </> );}
Tool that sends a toast notification
The following snippet shows a tool that lets AI send a toast notification with
Sonner, then adds a message in the chat with
AiTool, letting the user
know that a toast was sent.
import { AiChat } from "@liveblocks/react-ui";import { RegisterAiTool } from "@liveblocks/react";import { defineAiTool } from "@liveblocks/client";import { toast, Toaster } from "sonner";
function Chat() { return ( <> <RegisterAiTool name="send-toast-notification" tool={defineAiTool()({ description: "Send a toast notification",
parameters: { type: "object", properties: { message: { type: "string", description: "The message to display in the toast", }, }, required: ["message"], additionalProperties: false, },
execute: async ({ message }) => { toast(message); return { data: { message }, description: "You sent a toast", }; },
render: () => <AiTool title="Toast sent" icon="🍞" />, })} /> <AiChat chatId="my-chat-id" /> <Toaster /> </> );}
Scoping a tool to a specific chat
Tools can be scoped to specific chats by providing a chatId prop. When scoped,
the tool will only be available to that specific chat.
import { AiChat } from "@liveblocks/react-ui";import { RegisterAiTool } from "@liveblocks/react";import { defineAiTool } from "@liveblocks/client";
function Chat() { return ( <> <RegisterAiTool name="private-tool" tool={defineAiTool()({ // Tool definition // ... })} chatId="my-chat" /> <AiChat chatId="my-chat" /> </> );}
Props
- namestringRequired
Unique name for the tool. This is used internally to identify and manage the tool.
- toolAiOpaqueToolDefinitionRequired
The tool definition created with
defineAiTool.
- chatIdstring
Optional chat ID to scope this tool to a specific chat. If provided, the tool will only be available to that chat.
- enabledboolean
Whether this tool should be enabled. When set to false, the tool will not
be made available to the AI copilot for any new/future chat messages, but
will still allow existing tool invocations to be rendered that are part of
the historic chat record. When provided as a prop to RegisterAiTool, it
will take precedence over the value of the tool’s enabled value in
defineAiTool.
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
Sync Datastore, 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 istrue
Whether the room immediately connects to Liveblocks servers. This value is ignored after the first render.
- engine1 | 2
Preferred storage engine version to use when creating the room. Only takes effect if the room doesn’t exist yet. The v2 Storage engine supports larger documents, is more performant, has native streaming support, and will become the default in the future. Learn\ more.
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.
initialStorage is only read and set a single time, unless a new top-level
property is added.
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> );}
If a new top-level property is added to initialStorage, the next time a user
connects, the new property will be created. Other properties will be unaffected.
Any
conflict-free live structures
and JSON-serializable objects are allowed (the LsonObject type).
Speed up connecting to a room
To speed up connecting to a room, you can call
Liveblocks.prewarmRoom
on the server, which will warm up a room for the next 10 seconds. Triggering
this directly before a user navigates to a room is an easy to way use this API.
Here’s a Next.js server actions example, showing how to trigger prewarming with
onPointerDown.
actions.ts
Sign in to get your own API keys.
"use server";
import { Liveblocks } from "@liveblocks/node";
const liveblocks = new Liveblocks({ secret: "sk_prod_xxxxxxxxxx…xxxxxxxxsk_prod_xxxxxxxxxxxxxxxxxxxxxxxx",});
export async function prewarmRoom(roomId: string) { await liveblocks.prewarmRoom(roomId);}
RoomLink.tsx
"use client";
import { prewarmRoom } from "../actions";import Link from "next/link";
export function JoinButton({ roomId }: { roomId: string }) { return ( <Link href={`/rooms/${roomId}`} onPointerDown={() => prewarmRoom(roomId)}> {roomId} </Link> );}
onPointerDown is slightly quicker than onClick because it triggers before
the user releases their pointer.
createRoomContext
This used to be the default way to start your app, but now it’s recommend for
advanced usage only. We generally recommend using LiveblocksProvider and
following typing your data with the Liveblocks interface,
unless you need to define multiple room types in your application.
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 = {};type CommentMetadata = {};
export const { RoomProvider, useMyPresence,
// Other hooks // ...} = createRoomContext< Presence, Storage, UserMeta, RoomEvent, ThreadMetadata, CommentMetadata>(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 = {};type CommentMetadata = {};
export const { suspense: { RoomProvider, useMyPresence,
// Other suspense hooks // ... },} = createRoomContext< Presence, Storage, UserMeta, RoomEvent, ThreadMetadata, CommentMetadata>(client);
Typing createRoomContext
To type your hooks, you can pass multiple different types to
createRoomContext. A full explanation is in the code snippet below.
liveblocks.config.ts
useRoomRoomProvider
Returns the Room of the nearest RoomProvider above in the React
component tree.
import { useRoom } from "@liveblocks/react/suspense";
const room = useRoom();
Will throw when used outside of a RoomProvider. If you don’t want this
hook to throw when used outside of a Room context (for example to write
components in a way that they can be used both inside and outside of a
Liveblocks room), you can use the { allowOutsideRoom } option:
import { useRoom } from "@liveblocks/react/suspense";
const room = useRoom({ allowOutsideRoom: true }); // Possibly `null`
Options
- allowOutsideRoombooleanDefault isfalse
Whether the hook should return null instead of throwing when used outside
of a RoomProvider context.
Returns
- roomRoom
The Room instance from the nearest RoomProvider. Returns null if
allowOutsideRoom is true and the hook is used outside of a room.
useIsInsideRoomBoth
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();
Arguments
None
Returns
- isInsideRoomboolean
true if the hook was called inside a RoomProvider context, false
otherwise.
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 />
useStatusRoomProvider
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();
Arguments
None
Returns
- status'initial' | 'connecting' | 'connected' | 'reconnecting' | 'disconnected'
The current WebSocket connection status of the room.
useSyncStatusBoth
Returns the current synchronization status of Liveblocks, and will re-render your component whenever it changes. This includes any part of Liveblocks that may be synchronizing local changes to the server, including (any room’s) Storage, text editors, threads, or notifications.
A { smooth: true } option is also available, which prevents quick changes
between states, making it ideal for
rendering a synchronization badge in your app.
Suspense and
regular versions of this
hook are available.
import { useSyncStatus } from "@liveblocks/react/suspense";
// "synchronizing" | "synchronized"const syncStatus = useSyncStatus();
Arguments
- optionsobject
Optional configuration object.
- options.smoothbooleanDefault isfalse
When true, prevents quick changes between states by delaying the
transition from "synchronizing" to "synchronized" until 1 second has passed
after the final change.
Returns
- syncStatus'synchronizing' | 'synchronized'
The current synchronization status of Liveblocks.
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 synchronization status badge, as it won’t flicker in
a distracting manner when changes are made in quick succession.
import { useSyncStatus } from "@liveblocks/react/suspense";
function StorageStatusBadge() { const syncStatus = useSyncStatus({ smooth: true });
return <div>{syncStatus === "synchronized" ? "✅ Saved" : "🔄 Saving"}</div>;}
Prevent users losing unsaved changes
function Page() { return ( <LiveblocksProvider preventUnsavedChanges
// Other options // ... > ... </LiveblocksProvider> );}
More specifically, this option triggers when:
Internally, this option uses the beforeunload event.
useOthersListenerRoomProvider
Calls the given callback when an “others” event occurs, when a user enters, leaves, or updates their presence.
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; } });}
Arguments
- callback(event: OthersEvent) => void
A callback function that is called when an "others" event occurs. The event
object contains the error type, the user that triggered it, and the current
others in the room. 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.
useLostConnectionListenerRoomProvider
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.
Arguments
- callback(event: 'lost' | 'restored' | 'failed') => void
A callback function that is called when a connection loss event occurs. The event can be "lost", "restored", or "failed".
Presence
Need help troubleshooting presence?
Try the Liveblocks DevTools extension to visualize your collaborative experiences as you build them, in realtime.
useMyPresenceRoomProvider
Return the presence of the current user, and a function to update it. Automatically subscribes to updates to the current user’s presence. Setting a property will not replace the whole state, but will instead merge the property into the existing state.
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 }"
Returns
- myPresenceTPresence
The current user’s presence data.
- updateMyPresence(patch: Partial
, options?: { addToHistory?: boolean }) => void
A function to update the current user’s presence. Accepts a partial presence object and optional history options.
Adding presence to history
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 });
Other ways to use presence
useMyPresence is a more convenient way to update and view presence, rather
than using useSelf and useUpdateMyPresence in combination.
const myPresence = useSelf((me) => me.presence);const updateMyPresence = useUpdateMyPresence();
useUpdateMyPresenceRoomProvider
Returns a setter function to update the current user’s presence. Setting a
property will not replace the whole state, but will instead merge the property
into the existing state. Will trigger fewer renders than useMyPresence, as
it doesn’t update when presence changes.
import { useUpdateMyPresence } from "@liveblocks/react/suspense";
const updateMyPresence = useUpdateMyPresence();
updateMyPresence({ y: 0 });updateMyPresence({ x: 0 });
// Presence will be { x: 0, y: 0 }
Arguments
None
Returns
- updateMyPresence(patch: Partial
, options?: { addToHistory?: boolean }) => void
A function to update the current user's presence. Accepts a partial presence object and optional history options.
Adding presence to history
updateMyPresence({ selectedId: "xxx" }, { addToHistory: true });
useSelfRoomProvider
Returns the current user once it is connected to the room, and automatically subscribes to updates to the current user. Suspense and regular versions of this hook are available.
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.
Arguments
- selector(me: User) => T
Optional selector function to extract specific data from the current user. If not provided, returns the entire user object.
Returns
- currentUserUser | T | null
The current user object or the selected data from the user. Returns null
if not connected to the room (in non-Suspense version).
Checking user permissions
It’s possible to check if a user has a specific permission by using the
canWrite and canComment properties of the User object. This is set via
your room permissions.
import { useSelf } from "@liveblocks/react/suspense";
function PermissionBadge() { const canWrite = useSelf((me) => me.canWrite);
if (canWrite) { return <div>✏️ Full access</div>; }
return <div>👀 Read-only access</div>;}
This is particularly helpful in combination with text editors, such as Tiptap as you can prevent read-only users from editing the document.
import { useSelf } from "@liveblocks/react/suspense";import { useLiveblocksExtension } from "@liveblocks/react-tiptap";import { useEditor, EditorContent } from "@tiptap/react";
function TextEditor() { const liveblocks = useLiveblocksExtension(); const canWrite = useSelf((me) => me.canWrite);
const editor = useEditor({ editable: canWrite, extensions: [ liveblocks, // ... ], });
return ( <div> <EditorContent editor={editor} /> </div> );}
useOthersRoomProvider
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. Suspense and regular versions of this hook are available.
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 // 👈);
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 } } },// ]
Caution
In production-ready apps, you likely want to avoid calling useOthers without
arguments.
Arguments
- selector(others: readonly User[]) => T
Optional selector function to extract specific data from the others array. If not provided, returns the entire others array.
- isEqual(prev: T, curr: T) => boolean
Optional equality function to determine if the selected data has changed. Defaults to strict equality comparison.
Returns
- othersreadonly TUser[] | null
The others array or the selected data from the others array. Returns null
if not connected to the room (in non-Suspense version).
useOthersMappedRoomProvider
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} /> ))} </>);
Arguments
- selector(other: User) => T
A selector function to extract specific data from each user in the others array.
- isEqual(prev: T, curr: T) => boolean
Optional equality function to determine if the selected data for a user has changed. Defaults to strict equality comparison.
Returns
- othersreadonly [connectionId: number, data: T][] | null
An array of tuples where each item is a pair of [connectionId, selectedData]. Returns null if not connected to the room (in non-Suspense
version).
useOthersConnectionIdsRoomProvider
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.
Suspense and
regular versions of this
hook are available.
import { useOthersConnectionIds } from "@liveblocks/react/suspense";
// [2, 4, 7]const connectionIds = useOthersConnectionIds();
Returns
- connectionIdsreadonly number[] | null
An array of connection IDs for all other users in the room. Returns null
if not connected to the room (in non-Suspense version).
Another way to fetch connection IDs
This hook is similar to using useOthers and calling .map() on the
result.
import { useOthers, shallow } from "@liveblocks/react/suspense";
// [2, 4, 7]const connectionIds = useOthers( (others) => others.map((other) => other.connectionId), shallow);
useOtherRoomProvider
Extract data using a selector for one specific user in the room, and subscribe to all changes to the selected data. Suspense and regular versions of this hook are available.
import { useOther } from "@liveblocks/react/suspense";
// ✅ 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} />;}
- Makes sure this whole component tree will never rerender beyond the first time.
- Makes sure the parent component only rerenders when users join/leave.
- Makes sure each cursor remains associated to the same connection.
- Makes sure each cursor rerenders whenever its data changes only.
👉 A Suspense version of this hook is also available, which will never
return null.
Arguments
- connectionIdnumber
The connection ID of the specific user to extract data from.
- selector(other: User) => T
A selector function to extract specific data from the user.
- isEqual(prev: T, curr: T) => boolean
Optional equality function to determine if the selected data has changed. Defaults to strict equality comparison.
Returns
- dataT | null
The selected data from the specified user. Returns null if the user is not
found or not connected to the room (in non-Suspense version).
Broadcast
useBroadcastEventRoomProvider
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 }});
Arguments
None
Returns
- broadcast(event: TBroadcastEvent) => void
A function that broadcasts custom events to other users in the room.
useEventListenerRoomProvider
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.
Arguments
- callback(event: { event: TBroadcastEvent; user: User | null; connectionId: number }) => void
A callback function that is called when a custom event is received. The
callback receives an object with the event data, user information, and
connection ID. Connection ID is always -1 when receiving an event sent
from the server.
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, aPersonwith aname: stringand anage: numberfield. 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 ofPersonvalues 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>; }; }}
You can then set an initial value in RoomProvider.
import { LiveList } from "@liveblocks/client";import { RoomProvider } from "@liveblocks/react/suspense";
function App() { return ( <RoomProvider id="my-room-name" initialStorage={{ animals: new LiveList(["Fido"]) }} > {/* children */} </RoomProvider> );}
The type received in your Storage will match the type passed. Learn more under typing your data.
import { useMutation } from "@liveblocks/react/suspense";
function App() { const addAnimal = useMutation(({ storage }) => { const animals = storage.get("animals");
// LiveList<["Fido"]> console.log(animals);
animals.push("Felix");
// LiveList<["Fido", "Felix"]> console.log(animals); });
return <button onClick={addAnimal}>Add animal</button>;}
useStorage will return an immutable copy of the data, for example a
LiveList is converted to an array, which makes it easy to render.
import { useStorage } from "@liveblocks/react/suspense";
function App() { const animals = useStorage((root) => root.animals);
// ["Fido", "Felix"] console.log(animals);
return ( <ul> {animals.map((animal) => ( <li key={animal}>{animal}</li> ))} </ul> );}
Nesting data structures
All Storage data structures can be nested, allowing you to create complex trees of conflict-free data.
import { LiveObject, LiveList, LiveMap } from "@liveblocks/client";
type Person = LiveObject<{ name: string; pets: LiveList<string>;}>;
declare global { interface Liveblocks { Storage: { people: LiveMap<string, Person>; }; }}
Here’s an example of setting initialStorage for this type.
import { LiveObject, LiveList, LiveMap } from "@liveblocks/client";import { RoomProvider } from "@liveblocks/react/suspense";
function App() { return ( <RoomProvider id="my-room-name" initialStorage={{ people: new LiveMap([ [ "alicia", new LiveObject({ name: "Alicia", pets: new LiveList(["Fido", "Felix"]), }), ], ]), }} > {/* children */} </RoomProvider> );}
Need help troubleshooting Storage?
Get the Liveblocks DevTools extension to develop and debug your application as you build it.
useStorageRoomProvider
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.
Avoiding unnecessary rerenders
It’s recommended to select only the subset of Storage data that your component needs. This will avoid unnecessary rerenders that happen with overselection.
Arguments
- selector(root: ToJson
) => T
A selector function to extract specific data from the storage root. The root is an immutable copy of your entire Liveblocks Storage tree.
- isEqual(prev: T, curr: T) => boolean
Optional equality function to determine if the selected data has changed. Defaults to strict equality comparison.
Returns
- dataT | null
The selected data from storage. Returns null while storage is still
loading (in non-Suspense version).
useHistoryRoomProvider
Returns the room’s history. See Room.history for more information.
import { useHistory } from "@liveblocks/react/suspense";
const { undo, redo, pause, resume, disable } = useHistory();
Arguments
None
Returns
- historyHistory
The room's history object containing methods for
undo,
redo,
pause,
resume, and
disable
operations.
useUndoRoomProvider
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/react/suspense";
const undo = useUndo();
Arguments
None
Returns
- undo() => void
A function that undoes the last operation executed by the current client.
useRedoRoomProvider
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/react/suspense";
const redo = useRedo();
Arguments
None
Returns
- redo() => void
A function that redoes the last operation executed by the current client.
useCanUndoRoomProvider
Returns whether there are any operations to undo.
import { useCanUndo, useUpdateMyPresence } from "@liveblocks/react/suspense";
const updateMyPresence = useUpdateMyPresence();const canUndo = useCanUndo();
updateMyPresence({ y: 0 });
// At the next render, "canUndo" will be true
Arguments
None
Returns
- canUndoboolean
Whether there are any operations to undo.
useCanRedoRoomProvider
Returns whether there are any operations to redo.
import { useCanRedo, useUndo, useUpdateMyPresence,} from "@liveblocks/react/suspense";
const updateMyPresence = useUpdateMyPresence();const undo = useUndo();const canRedo = useCanRedo();
updateMyPresence({ y: 0 });undo();
// At the next render, "canRedo" will be true
Arguments
None
Returns
- canRedoboolean
Whether there are any operations to redo.
useMutationRoomProvider
Creates a callback function that lets you mutate Liveblocks state.
import { useMutation } from "@liveblocks/react/suspense";
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:
- Close over a local variable and adding it to the dependency array, or
- Have it take an extra callback parameter.
Both are equally fine, just a matter of preference.
Arguments
- mutationFn(context: MutationContext, ...args: TArgs) => void
A function that performs mutations on Liveblocks state. The context provides
access to storage, setMyPresence, self, and others.
- depsReact.DependencyList
A dependency array that determines when the mutation function should be
recreated, similar to useCallback.
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} />;
Tip! Let ESLint check your dependencies
If you use ESLint, we recommend to configure it to enforce the correct use of your dependency arrays.
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" }] }}
Returns
- mutate(...args: TArgs) => void
A memoized callback function that executes the mutation. Can accept additional arguments that are passed to the mutation function.
Comments
useThreadsRoomProvider
Returns a paginated list of threads within the current room. Initially fetches the oldest 50 threads. Suspense and regular versions of this hook are available.
import { useThreads } from "@liveblocks/react";
const { threads, error, isLoading } = useThreads();
Use the Thread component to
render the oldest 50 threads with our default UI.
import { Thread } from "@liveblocks/react-ui";import { useThreads } from "@liveblocks/react/suspense";
function Component() { const { threads } = useThreads();
return ( <div> {threads.map((thread) => ( <Thread key={thread.id} thread={thread} /> ))} </div> );}
Arguments
- optionsobject
Optional configuration object.
- option.queryThreadsQuery
Optional query to filter threads by resolved status and metadata values. Learn more.
- option.scrollOnLoadboolean
Whether to scroll to a comment if the URL's hash is set to a comment ID.
Defaults to true. Learn\
more.
Returns
- threadsThreadData[] | undefined
An array of threads within the current room, or undefined if not yet
loaded (in non-Suspense version).
- isLoadingboolean
Whether the threads are currently being loaded.
- errorError | undefined
Any error that occurred while loading the threads.
- hasFetchedAllboolean
Whether all available threads have been fetched. Learn\ more.
- fetchMore() => void
A function to fetch more threads. Learn\ more.
- isFetchingMoreboolean
Whether more threads are currently being fetched. Learn\ more.
- fetchMoreErrorError | undefined
Any error that occurred while fetching more threads. Learn\ more.
Querying threads
It’s possible to return threads that match a certain query with the query
option. You can filter threads based on their resolved status, if the user is
subscribed to them, and metadata. Additionally, you can filter for metadata
strings that being with certain characters using startsWith and you can filter
for metadata numbers using gt, lt, gte, and lte. Returned threads match
the entire query.
// Returns threads that match the entire `query`, e.g. { color: "blue", pinned: true, ... }const { threads } = useThreads({ query: { // Filter for unresolved threads resolved: false,
// Filter for threads that the user is subscribed to subscribed: true,
metadata: { // Filter for threads that contain specific string, boolean, and number data color: "blue", pinned: true, priority: 3,
// Filter for threads with string metadata that starts with a certain value organization: { startsWith: "liveblocks:", },
// Filter for threads with number metadata that is greater than 50 and lower than 100 posX: { gt: 50, lt: 100, },
// Filter for threads with number metadata that is greater than or equal to 5 level: { gte: 5, }, }, },});
Pagination
By default, the useThreads hook returns up to 50 threads. To fetch more, the
hook provides additional fields for pagination, similar to
useInboxNotifications.
import { useThreads } from "@liveblocks/react";
const { isLoading, error, threads,
fetchMore, isFetchingMore, hasFetchedAll, fetchMoreError,} = useThreads();
hasFetchedAllindicates whether all available threads have been fetched.fetchMoreloads up to 50 more threads, and is always safe to call.isFetchingMoreindicates whether more threads are being fetched.fetchMoreErrorreturns error statuses resulting from fetching more.
Pagination example
The following example demonstrates how to use the fetchMore function to
implement a “Load More” button, which fetches additional threads when clicked.
The button is disabled while fetching is in progress.
import { Thread } from "@liveblocks/react-ui";import { useThreads } from "@liveblocks/react/suspense";
function Threads() { const { threads, hasFetchedAll, fetchMore, isFetchingMore } = useThreads();
return ( <div> {threads.map((thread) => ( <Thread key={thread.id} thread={thread} /> ))} {hasFetchedAll ? ( <div>🎉 You've loaded all threads!</div> ) : ( <button disabled={isFetchingMore} onClick={fetchMore}> Load more </button> )} </div> );}
Error handling
Error handling is another important aspect to consider when using the
useThreads hook. The error and fetchMoreError fields provide information
about any errors that occurred during the initial fetch or subsequent fetch
operations, respectively. You can use these fields to display appropriate error
messages to the user and implement retry mechanisms if needed.
The following example shows how to display error messages for both initial loading errors and errors that occur when fetching more threads.
import { Thread } from "@liveblocks/react-ui";import { useThreads } from "@liveblocks/react";
function Inbox() { const { threads, error, fetchMore, fetchMoreError } = useThreads();
// Handle error if the initial load failed. // The `error` field is not returned by the Suspense hook as the error is thrown to nearest ErrorBoundary if (error) { return ( <div> <p>Error loading threads: {error.message}</p> </div> ); }
return ( <div> {threads.map((thread) => ( <Thread key={thread.id} thread={thread} /> ))}
{fetchMoreError && ( <div> <p>Error loading more threads: {fetchMoreError.message}</p> <button onClick={fetchMore}>Retry</button> </div> )} </div> );}
Avoid scrolling to a comment
By default, scrollOnLoad, is enabled. This options scrolls to a comment if the
URL’s hash is set to a comment ID (e.g.
https://example.com/my-room#cm_nNJs9sb...), the page will scroll to that
comment once the threads are loaded. To avoid scrolling to a comment, set
scrollOnLoad to false.
const { threads } = useThreads({ scrollOnLoad: false });
useCreateThreadRoomProvider
Returns a function that optimistically creates a thread with an initial comment, and optionally some thread and comment metadata.
import { useCreateThread } from "@liveblocks/react/suspense";
const createThread = useCreateThread();const thread = createThread({ body: {}, attachments: [], metadata: {}, commentMetadata: {},});
Returns
- createThread(options: CreateThreadOptions) => ThreadData
A function that creates a thread with an initial comment, and optionally thread and comment metadata. Returns the optimistic thread object.
Error handling
useCreateThread creates threads optimistically, meaning that a thread object
is returned instantly, before Liveblocks has confirmed a successful thread
creation. To catch any errors that occur, add useErrorListener and look
for the CREATE_THREAD_ERROR type.
import { useErrorListener } from "@liveblocks/react/suspense";
useErrorListener((error) => { if (error.context.type === "CREATE_THREAD_ERROR") { const { roomId, threadId, commentId, body, metadata } = error.context; console.log(`Problem creating thread ${threadId}`); }});
useSearchCommentsRoomProvider
Search comments in the current room using semantic search and various filters. Returns a list of results, including the ID and the plain text content of matched comments, and the thread ID of the comment’s parent. Use it to create a search bar.
import { useSearchComments } from "@liveblocks/react";
const { results, error, isLoading } = useSearchComments({ query: { text: "fruit" },});
// [{ content: "I like apples", threadId: "th_xxx", commentId: "cm_xxx" }, ...]console.log(results);
Its semantic search finds results based on meaning, so a query like “fruit” also brings up related items such as “apples” or “oranges”, even if the exact words don’t match.
Arguments
- optionsobject
Configuration object.
- options.querySearchCommentsQuery
Optional query to filter comments by metadata and resolved status of the parent thread, and presence and absence of attachments and mentions in the comment.
- options.query.textstringRequired
Text to search within comment content. Uses rich text and vector search for relevance.
- options.query.threadMetadataPartial
Metadata to filter threads by.
- options.query.threadResolvedboolean
Whether to only return comments from threads that are resolved.
- options.query.hasAttachmentsboolean
Whether to only return comments that have attachments.
- options.query.hasMentionsboolean
Whether to only return comments that have mentions.
Returns
- resultsThreadData[] | undefined
An array of matched comments with the parent thread they belong to.
- isLoadingboolean
Whether the results are currently being loaded.
- errorError | undefined
Any error that occurred while searching the threads.
Create a comments search bar
useSearchComments allows you to create a search bar for comments that exist
within the current room. Linking to the commentId with a hash will highlight
the comment on the page.
import { useSearchComments } from "@liveblocks/react";import { useState } from "react";
function SearchBar() { const [search, setSearch] = useState("");
const { results, isLoading, error } = useSearchComments({ query: { text: search }, });
return ( <> <input type="text" value={search} onChange={(e) => setSearch(e.target.value)} /> {isLoading ? ( <div>Loading...</div> ) : ( results.map((result) => ( <a key={result.commentId} href={"#" + result.commentId}> {result.content} </a> )) )} </> );}
You could also add buttons that apply various filters, such as “Resolved” and “Unresolved”.
Resolved/unresolved filter
useDeleteThreadRoomProvider
Returns a function that deletes a thread and all its associated comments by ID. Only the thread creator can delete the thread.
import { useDeleteThread } from "@liveblocks/react/suspense";
const deleteThread = useDeleteThread();deleteThread("th_xxx");
Returns
- deleteThread(threadId: string) => void
A function that deletes a thread and all its associated comments by ID.
Error handling
useDeleteThread deletes threads optimistically, meaning that the thread
appears deleted instantly, before Liveblocks has confirmed a successful thread
deletion. To catch any errors that occur, add useErrorListener and look
for the DELETE_THREAD_ERROR type.
import { useErrorListener } from "@liveblocks/react/suspense";
useErrorListener((error) => { if (error.context.type === "DELETE_THREAD_ERROR") { const { roomId, threadId } = error.context; console.log(`Problem deleting thread ${threadId}`); }});
useEditThreadMetadataRoomProvider
Returns a function that edits a thread’s metadata. To delete an existing
metadata property, set its value to null. Passing undefined for a metadata
property will ignore it.
import { useEditThreadMetadata } from "@liveblocks/react/suspense";
const editThreadMetadata = useEditThreadMetadata();editThreadMetadata({ threadId: "th_xxx", metadata: {} });
Returns
- editThreadMetadata(options: { threadId: string; metadata: ThreadMetadata }) => void
A function that edits a thread’s metadata. To delete an existing metadata
property, set its value to null.
Error handling
useEditThreadMetadata edits thread metadata optimistically, meaning that the
metadata appears updated instantly, before Liveblocks has confirmed a successful
metadata update. To catch any errors that occur, add useErrorListener and
look for the EDIT_THREAD_METADATA_ERROR type.
import { useErrorListener } from "@liveblocks/react/suspense";
useErrorListener((error) => { if (error.context.type === "EDIT_THREAD_METADATA_ERROR") { const { roomId, threadId, metadata } = error.context; console.log(`Problem editing thread metadata ${threadId}`); }});
useMarkThreadAsResolvedRoomProvider
Returns a function that marks a thread as resolved.
import { useMarkThreadAsResolved } from "@liveblocks/react/suspense";
const markThreadAsResolved = useMarkThreadAsResolved();markThreadAsResolved("th_xxx");
Returns
- markThreadAsResolved(threadId: string) => void
A function that marks a thread as resolved.
Error handling
useMarkThreadAsResolved marks threads as resolved optimistically, meaning that
the thread appears resolved instantly, before Liveblocks has confirmed the
successful status change. To catch any errors that occur, add
useErrorListener and look for the MARK_THREAD_AS_RESOLVED_ERROR type.
import { useErrorListener } from "@liveblocks/react/suspense";
useErrorListener((error) => { if (error.context.type === "MARK_THREAD_AS_RESOLVED_ERROR") { const { roomId, threadId } = error.context; console.log(`Problem marking thread as resolved ${threadId}`); }});
useMarkThreadAsUnresolvedRoomProvider
Returns a function that marks a thread as unresolved.
import { useMarkThreadAsUnresolved } from "@liveblocks/react/suspense";
const markThreadAsUnresolved = useMarkThreadAsUnresolved();markThreadAsUnresolved("th_xxx");
Returns
- markThreadAsUnresolved(threadId: string) => void
A function that marks a thread as unresolved.
Error handling
useMarkThreadAsUnresolved marks threads as unresolved optimistically, meaning
that the thread appears unresolved instantly, before Liveblocks has confirmed
the successful status change. To catch any errors that occur, add
useErrorListener and look for the MARK_THREAD_AS_UNRESOLVED_ERROR type.
import { useErrorListener } from "@liveblocks/react/suspense";
useErrorListener((error) => { if (error.context.type === "MARK_THREAD_AS_UNRESOLVED_ERROR") { const { roomId, threadId } = error.context; console.log(`Problem marking thread as unresolved ${threadId}`); }});
useMarkThreadAsReadRoomProvider
Returns a function that marks a thread as read.
import { useMarkThreadAsRead } from "@liveblocks/react/suspense";
const markThreadAsRead = useMarkThreadAsRead();markThreadAsRead("th_xxx");
Returns
- markThreadAsRead(threadId: string) => void
A function that marks a thread as read.
useThreadSubscriptionRoomProvider
Returns the subscription status of a thread, methods to update it, and when the thread was last read. The subscription status affects whether the current user receives inbox notifications when new comments are posted.
import { useThreadSubscription } from "@liveblocks/react/suspense";
const { status, subscribe, unsubscribe, unreadSince } = useThreadSubscription("th_xxx");
subscribe and unsubscribe work similarly to
useSubscribeToThread and
useUnsubscribeFromThread, but they only affect
the current thread.
Arguments
- threadIdstring
The ID of the thread to get subscription status for.
Returns
- statusThreadSubscriptionStatus
The subscription status of the thread ('subscribed', 'unsubscribed', or 'not_subscribed').
- subscribe() => void
A function to subscribe to the thread.
- unsubscribe() => void
A function to unsubscribe from the thread.
- unreadSinceDate | null
The date when the thread was last read, or null if it has been read.
useSubscribeToThreadRoomProvider
Returns a function that subscribes the current user to a thread, meaning they will receive inbox notifications when new comments are posted.
import { useSubscribeToThread } from "@liveblocks/react/suspense";
const subscribeToThread = useSubscribeToThread();subscribeToThread("th_xxx");
Error handling
useSubscribeToThread subscribes to threads optimistically, meaning that the
subscription appears active instantly, before Liveblocks has confirmed a
successful subscription. To catch any errors that occur, add
useErrorListener and look for the SUBSCRIBE_TO_THREAD_ERROR type.
import { useErrorListener } from "@liveblocks/react/suspense";
useErrorListener((error) => { if (error.context.type === "SUBSCRIBE_TO_THREAD_ERROR") { const { roomId, threadId } = error.context; console.log(`Problem subscribing to thread ${threadId}`); }});
Subscribing will replace any existing subscription for the current thread set at room-level. This value can also be overridden by a room-level call that is run afterwards.
const subscribeToThread = useSubscribeToThread();const [{ settings }, updateSettings] = useRoomSubscriptionSettings();
// 1. Disables notifications for all threadsupdateSettings({ threads: "none",});
// 2. Enables notifications just for this thread, "th_d75sF3..."subscribeToThread("th_d75sF3...");
// 3. Disables notifications for all threads, including "th_d75sF3..."updateSettings({ threads: "none",});
Returns
- subscribeToThread(threadId: string) => void
A function that subscribes the current user to a thread for inbox notifications.
useUnsubscribeFromThreadRoomProvider
Returns a function that unsubscribes the current user from a thread, meaning they will no longer receive inbox notifications when new comments are posted.
import { useUnsubscribeFromThread } from "@liveblocks/react/suspense";
const unsubscribeFromThread = useUnsubscribeFromThread();unsubscribeFromThread("th_xxx");
Unsubscribing will replace any existing subscription for the current thread set at room-level. This value can also be overridden by a room-level call that is run afterwards.
const subscribeToThread = useSubscribeToThread();const [{ settings }, updateSettings] = useRoomSubscriptionSettings();
// 1. Enables notifications for all threadsupdateSettings({ threads: "all",});
// 2. Disables notifications just for this thread, "th_d75sF3..."subscribeToThread("th_d75sF3...");
// 3. Enables notifications for all threads, including "th_d75sF3..."updateSettings({ threads: "all",});
Returns
- unsubscribeFromThread(threadId: string) => void
A function that unsubscribes the current user from a thread, stopping inbox notifications.
Error handling
useUnsubscribeFromThread unsubscribes from threads optimistically, meaning
that the subscription appears inactive instantly, before Liveblocks has
confirmed a successful unsubscription. To catch any errors that occur, add
useErrorListener and look for the UNSUBSCRIBE_FROM_THREAD_ERROR type.
import { useErrorListener } from "@liveblocks/react/suspense";
useErrorListener((error) => { if (error.context.type === "UNSUBSCRIBE_FROM_THREAD_ERROR") { const { roomId, threadId } = error.context; console.log(`Problem unsubscribing from thread ${threadId}`); }});
useCreateCommentRoomProvider
Returns a function that adds a comment to a thread.
import { useCreateComment } from "@liveblocks/react/suspense";
const createComment = useCreateComment();const comment = createComment({ threadId: "th_xxx", body: {}, attachments: [], metadata: {},});
Returns
- createComment(options: CreateCommentOptions) => CommentData
A function that adds a comment to a thread. Returns the optimistic comment object.
Error handling
useCreateComment creates comments optimistically, meaning that a comment
object is returned instantly, before Liveblocks has confirmed a successful
comment creation. To catch any errors that occur, add useErrorListener and
look for the CREATE_COMMENT_ERROR type.
import { useErrorListener } from "@liveblocks/react/suspense";
useErrorListener((error) => { if (error.context.type === "CREATE_COMMENT_ERROR") { const { roomId, threadId, commentId, body } = error.context; console.log(`Problem creating comment ${commentId}`); }});
useEditCommentRoomProvider
Returns a function that edits a comment’s body, and optionally its attachments and metadata.
import { useEditComment } from "@liveblocks/react/suspense";
const editComment = useEditComment();editComment({ threadId: "th_xxx", commentId: "cm_xxx", body: {}, attachments: [], metadata: {},});
Returns
- editComment(options: EditCommentOptions) => void
A function that edits a comment’s body, and optionally its attachments and metadata.
Error handling
useEditComment edits comments optimistically, meaning that the comment appears
updated instantly, before Liveblocks has confirmed a successful comment edit. To
catch any errors that occur, add useErrorListener and look for the
EDIT_COMMENT_ERROR type.
import { useErrorListener } from "@liveblocks/react/suspense";
useErrorListener((error) => { if (error.context.type === "EDIT_COMMENT_ERROR") { const { roomId, threadId, commentId, body } = error.context; console.log(`Problem editing comment ${commentId}`); }});
useEditCommentMetadataRoomProvider
Returns a function that edits a comment’s metadata. To delete an existing
metadata property, set its value to null. Passing undefined for a metadata
property will ignore it.
import { useEditCommentMetadata } from "@liveblocks/react/suspense";
const editCommentMetadata = useEditCommentMetadata();editCommentMetadata({ threadId: "th_xxx", commentId: "cm_xxx", metadata: {},});
Returns
- editCommentMetadata(options: { threadId: string; commentId: string; metadata: CommentMetadata }) => void
A function that edits a comment’s metadata. To delete an existing metadata
property, set its value to null.
Error handling
useEditCommentMetadata edits comment metadata optimistically, meaning that the
metadata appears updated instantly, before Liveblocks has confirmed a successful
metadata update. To catch any errors that occur, add useErrorListener and
look for the EDIT_COMMENT_METADATA_ERROR type.
import { useErrorListener } from "@liveblocks/react/suspense";
useErrorListener((error) => { if (error.context.type === "EDIT_COMMENT_METADATA_ERROR") { const { roomId, threadId, commentId, metadata } = error.context; console.log(`Problem editing comment metadata ${commentId}`); }});
useDeleteCommentRoomProvider
Returns a function that deletes a comment. If it is the last non-deleted comment, the thread also gets deleted.
import { useDeleteComment } from "@liveblocks/react/suspense";
const deleteComment = useDeleteComment();deleteComment({ threadId: "th_xxx", commentId: "cm_xxx" });
Returns
- deleteComment(options: { threadId: string; commentId: string }) => void
A function that deletes a comment. If it is the last non-deleted comment, the thread also gets deleted.
Error handling
useDeleteComment deletes comments optimistically, meaning that the comment
appears deleted instantly, before Liveblocks has confirmed a successful comment
deletion. To catch any errors that occur, add useErrorListener and look
for the DELETE_COMMENT_ERROR type.
import { useErrorListener } from "@liveblocks/react/suspense";
useErrorListener((error) => { if (error.context.type === "DELETE_COMMENT_ERROR") { const { roomId, threadId, commentId } = error.context; console.log(`Problem deleting comment ${commentId}`); }});
useAddReactionRoomProvider
Returns a function that adds a reaction to a comment. Can be used to create an emoji picker or emoji reactions.
import { useAddReaction } from "@liveblocks/react/suspense";
const addReaction = useAddReaction();addReaction({ threadId: "th_xxx", commentId: "cm_xxx", emoji: "👍" });
Returns
- addReaction(options: { threadId: string; commentId: string; emoji: string }) => void
A function that adds a reaction to a comment.
Error handling
useAddReaction adds reactions optimistically, meaning that the reaction
appears instantly, before Liveblocks has confirmed a successful reaction
addition. To catch any errors that occur, add useErrorListener and look
for the ADD_REACTION_ERROR type.
import { useErrorListener } from "@liveblocks/react/suspense";
useErrorListener((error) => { if (error.context.type === "ADD_REACTION_ERROR") { const { roomId, threadId, commentId, emoji } = error.context; console.log(`Problem adding reaction ${emoji} to comment ${commentId}`); }});
useRemoveReactionRoomProvider
Returns a function that removes a reaction from a comment. Can be used to create an emoji picker or emoji reactions
import { useRemoveReaction } from "@liveblocks/react/suspense";
const removeReaction = useRemoveReaction();removeReaction({ threadId: "th_xxx", commentId: "cm_xxx", emoji: "👍" });
Returns
- removeReaction(options: { threadId: string; commentId: string; emoji: string }) => void
A function that removes a reaction from a comment.
Error handling
useRemoveReaction removes reactions optimistically, meaning that the reaction
disappears instantly, before Liveblocks has confirmed a successful reaction
removal. To catch any errors that occur, add useErrorListener and look for
the REMOVE_REACTION_ERROR type.
import { useErrorListener } from "@liveblocks/react/suspense";
useErrorListener((error) => { if (error.context.type === "REMOVE_REACTION_ERROR") { const { roomId, threadId, commentId, emoji } = error.context; console.log(`Problem removing reaction ${emoji} from comment ${commentId}`); }});
useAttachmentUrlRoomProvider
Returns a presigned URL for an attachment by its ID. Suspense and regular versions of this hook are available.
import { useAttachmentUrl } from "@liveblocks/react";
const { url, error, isLoading } = useAttachmentUrl("at_xxx");
Arguments
- attachmentIdstring
The ID of the attachment to get a presigned URL for.
Returns
- urlstring | undefined
The presigned URL for the attachment, or undefined if not found or not yet
loaded (in non-Suspense version).
- isLoadingboolean
Whether the URL is currently being loaded.
- errorError | undefined
Any error that occurred while loading the URL.
Feeds
useFeedsRoomProvider
Returns a paginated list of feeds within the current room. Results are sorted oldest first and can be filtered and paginated. Suspense and regular versions of this hook are available.
import { useFeeds } from "@liveblocks/react";
const { feeds, error, isLoading } = useFeeds();
Arguments
- optionsobject
Optional configuration object.
- options.sincenumber
Optional timestamp filter (ms). Only feeds whose createdAt or updatedAt
is at or after this value are included in feeds (applied to the cached
data for this hook’s options).
- options.metadataRecord<string, string>
Optional metadata filter. Only feeds whose metadata matches every key/value pair are included (applied to the cached data for this hook’s options).
- options.limitnumber
Page size for each server request when loading or loading more feeds. Does
not cap how many feeds appear in feeds—use fetchMore until
hasFetchedAll is true.
Returns
- feedsFeed[] | undefined
An array of feeds within the current room matching since and metadata
for this call, sorted by createdAt ascending (tie-break on feedId), or
undefined if not yet loaded (in non-Suspense version).
- isLoadingboolean
Whether the feeds are currently being loaded.
- errorError | undefined
Any error that occurred while loading the feeds.
- hasFetchedAllboolean
Whether all available feeds have been fetched.
- fetchMore() => void
A function to fetch more feeds.
- isFetchingMoreboolean
Whether more feeds are currently being fetched.
- fetchMoreErrorError | undefined
Any error that occurred while fetching more feeds.
Filtering
It’s possible to filter feeds by timestamp and metadata, and results are merged into a per-room cache. Limit sets the page size for each fetch when paginating.
const { feeds } = useFeeds({ // Optional, fetch feeds from the last day since: Date.now() - 1000 * 60 * 60 * 24,
// Optional, fetch feeds with the `{ tag: "design" }` metadata metadata: { tag: "design", },
// Optional, fetch only 10 at a time limit: 10,});
Pagination
By default, the useFeeds hook returns up to 50 feeds. To fetch more, the hook
provides additional fields for pagination, similar to useThreads.
import { useFeeds } from "@liveblocks/react";
const { feeds, error, isLoading,
fetchMore, isFetchingMore, hasFetchedAll, fetchMoreError,} = useFeeds();
hasFetchedAllindicates whether all available feeds have been fetched.fetchMoreloads up to 50 more feeds, and is always safe to call.isFetchingMoreindicates whether more feeds are being fetched.fetchMoreErrorreturns error statuses resulting from fetching more.
Pagination example
The following example demonstrates how to use the fetchMore function to
implement a “Load More” button, which fetches additional feeds when clicked. The
button is disabled while fetching is in progress.
import { Feed } from "@liveblocks/react-ui";import { useFeeds } from "@liveblocks/react/suspense";
function Feeds() { const { feeds, hasFetchedAll, fetchMore, isFetchingMore } = useFeeds();
return ( <div> {feeds.map((feed) => ( <a key={feed.feedId} href={`/feeds/${feed.feedId}`}> {feed.metadata.name} </a> ))} {hasFetchedAll ? ( <div>🎉 You've loaded all feeds!</div> ) : ( <button disabled={isFetchingMore} onClick={fetchMore}> Load more </button> )} </div> );}
useFeedMessagesRoomProvider
Returns a paginated list of messages for a specific feed in the current room.
Messages are sorted newest first using their createdAt property and can be
paginated.
Suspense and
regular versions of this
hook are available.
import { useFeedMessages } from "@liveblocks/react";
const { messages, error, isLoading } = useFeedMessages("my-feed-id");
Arguments
- feedIdstringRequired
The ID of the feed to get messages from.
- optionsobject
Optional configuration object.
- options.cursorstring
Optional cursor for pagination.
- options.limitnumber
Page size for each server request when loading or loading more messages.
Does not cap how many messages appear in messages—use fetchMore until
hasFetchedAll is true.
Returns
- messagesFeedMessage[] | undefined
An array of messages in the feed (chronological order by createdAt), or
undefined if not yet loaded (in non-Suspense version).
- isLoadingboolean
Whether the messages are currently being loaded.
- errorError | undefined
Any error that occurred while loading the messages.
- hasFetchedAllboolean
Whether all available messages have been fetched.
- fetchMore() => void
A function to fetch more messages.
- isFetchingMoreboolean
Whether more messages are currently being fetched.
- fetchMoreErrorError | undefined
Any error that occurred while fetching more messages.
Pagination
By default, the useFeedMessages hook returns up to 50 messages. To fetch more,
the hook provides additional fields for pagination, similar to useThreads.
import { useFeedMessages } from "@liveblocks/react";
const { messages, error, isLoading,
hasFetchedAll, fetchMore, isFetchingMore, fetchMoreError,} = useFeedMessages("my-feed-id");
hasFetchedAllindicates whether all available messages have been fetched.fetchMoreloads up to 50 more messages, and is always safe to call.isFetchingMoreindicates whether more messages are being fetched.fetchMoreErrorreturns error statuses resulting from fetching more.
Pagination example
The following example demonstrates how to use the fetchMore function to
implement a “Load More” button, which fetches additional messages when clicked.
The button is disabled while fetching is in progress.
import { useFeedMessages } from "@liveblocks/react/suspense";
function FeedMessages({ feedId }: { feedId: string }) { const { messages, hasFetchedAll, fetchMore, isFetchingMore } = useFeedMessages(feedId);
return ( <div> {messages.map((message) => ( <div key={message.id}>{message.data.content}</div> ))} {hasFetchedAll ? ( <div>🎉 You've loaded all messages!</div> ) : ( <button disabled={isFetchingMore} onClick={fetchMore}> Load more </button> )} </div> );}
useCreateFeedRoomProvider
Returns a function that creates a new feed in the current room.
import { useCreateFeed } from "@liveblocks/react";
const createFeed = useCreateFeed();createFeed("my-feed-id", { metadata: { name: "My Feed", channel: true }, timestamp: Date.now(),});
Returns
- createFeed(feedId: string, options?: { metadata?: JsonObject; timestamp?: number }) => void
A function that creates a feed. Takes the feed ID and optional metadata and timestamp.
useDeleteFeedRoomProvider
Returns a function that deletes a feed from the current room.
import { useDeleteFeed } from "@liveblocks/react";
const deleteFeed = useDeleteFeed();deleteFeed("my-feed-id");
Returns
- deleteFeed(feedId: string) => void
A function that deletes a feed. Takes the feed ID.
useUpdateFeedMetadataRoomProvider
Returns a function that updates a feed's metadata in the current room.
import { useUpdateFeedMetadata } from "@liveblocks/react";
const updateFeedMetadata = useUpdateFeedMetadata();updateFeedMetadata("my-feed-id", { name: "Updated Name", updated: new Date().toISOString(),});
Returns
- updateFeedMetadata(feedId: string, metadata: JsonObject) => void
A function that updates a feed's metadata. Takes the feed ID and the new metadata object.
useCreateFeedMessageRoomProvider
Returns a function that adds a new message to a feed in the current room.
import { useCreateFeedMessage } from "@liveblocks/react";
const createFeedMessage = useCreateFeedMessage();createFeedMessage("my-feed-id", { role: "user", content: "Hello, world!" });
// With optional id and timestampcreateFeedMessage( "my-feed-id", { role: "user", content: "Hello!" }, { id: "my-message-id", timestamp: Date.now(), });
Returns
- createFeedMessage(feedId: string, data: JsonObject, options?: { id?: string; timestamp?: number }) => void
A function that adds a message to a feed. Takes the feed ID, message data, and optional id and timestamp.
useDeleteFeedMessageRoomProvider
Returns a function that deletes a message from a feed in the current room.
import { useDeleteFeedMessage } from "@liveblocks/react";
const deleteFeedMessage = useDeleteFeedMessage();deleteFeedMessage("my-feed-id", "my-message-id");
Returns
- deleteFeedMessage(feedId: string, messageId: string) => void
A function that deletes a feed message. Takes the feed ID and message ID.
useUpdateFeedMessageRoomProvider
Returns a function that updates a feed message in the current room.
import { useUpdateFeedMessage } from "@liveblocks/react";
const updateFeedMessage = useUpdateFeedMessage();updateFeedMessage("my-feed-id", "my-message-id", { role: "user", content: "Updated content",});
Returns
- updateFeedMessage(feedId: string, messageId: string, data: JsonObject) => void
A function that updates a feed message. Takes the feed ID, message ID, and the new data object.
Typing feed metadata and message data
You can type feed metadata and feed message data using the Liveblocks
interface in your liveblocks.config.ts file. This provides type safety for
useFeeds and useFeedMessages.
// liveblocks.config.tsdeclare global { interface Liveblocks { FeedMetadata: { name?: string; channel?: boolean; agentName?: string; };
FeedMessageData: { role: "user" | "assistant" | "system"; content: string; }; }}
Notifications
useInboxNotificationsLiveblocksProvider
Returns a paginated list of inbox notifications for the current user. Initially fetches the latest 50 items. Inbox notifications are project-based, meaning notifications from outside the current room are received.
import { useInboxNotifications } from "@liveblocks/react";
const { inboxNotifications, error, isLoading } = useInboxNotifications();
Use the
InboxNotification
component to render the latest 50 inbox notifications with our default UI.
import { InboxNotification } from "@liveblocks/react-ui";import { useInboxNotifications } from "@liveblocks/react/suspense";
function Inbox() { const { inboxNotifications } = useInboxNotifications();
return ( <div> {inboxNotifications.map((notification) => ( <InboxNotification key={notification.id} inboxNotification={notification} /> ))} </div> );}
Arguments
- optionsobject
Optional configuration object.
- options.queryInboxNotificationsQuery
Optional query to filter notifications by room ID or kind. Learn\ more.
Returns
- inboxNotificationsInboxNotificationData[] | undefined
An array of inbox notifications for the current user, or undefined if not
yet loaded (in non-Suspense version).
- isLoadingboolean
Whether the notifications are currently being loaded.
- errorError | undefined
Any error that occurred while loading the notifications.
- hasFetchedAllboolean
Whether all available notifications have been fetched. Learn\ more.
- fetchMore() => void
A function to fetch more notifications. Learn\ more.
- isFetchingMoreboolean
Whether more notifications are currently being fetched. Learn\ more.
- fetchMoreErrorError | undefined
Any error that occurred while fetching more notifications. Learn\ more.
Querying inbox notifications
It’s possible to return inbox notifications that match a certain query with the
query option. You can filter inbox notifications based on their associated
room ID or kind.
// Returns inbox notifications that match the entire `query`, e.g. { roomId: "room1", ... }const { inboxNotifications } = useInboxNotifications({ query: { // Filter for roomId roomId: "room1", // Filter for kind kind: "thread", },});
Pagination
By default, the useInboxNotifications hook returns up to 50 notifications. To
fetch more, the hook provides additional fields for pagination, similar to
useThreads.
import { useInboxNotifications } from "@liveblocks/react";
const { inboxNotifications, isLoading, error,
hasFetchedAll, fetchMore, isFetchingMore, fetchMoreError,} = useInboxNotifications();
hasFetchedAllindicates whether all available inbox notifications have been fetched.fetchMoreloads up to 50 more notifications, and is always safe to call.isFetchingMoreindicates whether more notifications are being fetched.fetchMoreErrorreturns error statuses resulting from fetching more.
Pagination example
The following example demonstrates how to use the fetchMore function to
implement a “Load More” button, which fetches additional inbox notifications
when clicked. The button is disabled while fetching is in progress.
import { InboxNotification } from "@liveblocks/react-ui";import { useInboxNotifications } from "@liveblocks/react/suspense";
function Inbox() { const { inboxNotifications, hasFetchedAll, fetchMore, isFetchingMore } = useInboxNotifications();
return ( <div> {inboxNotifications.map((notification) => ( <InboxNotification key={notification.id} inboxNotification={notification} /> ))} {hasFetchedAll ? ( <div>🎉 You're all caught up!</div> ) : ( <button disabled={isFetchingMore} onClick={fetchMore}> Load more </button> )} </div> );}
Error handling
Error handling is another important aspect to consider when using the
useInboxNotifications hook. The error and fetchMoreError fields provide
information about any errors that occurred during the initial fetch or
subsequent fetch operations, respectively. You can use these fields to display
appropriate error messages to the user and implement retry mechanisms if needed.
The following example shows how to display error messages for both initial loading errors and errors that occur when fetching more inbox notifications.
import { InboxNotification } from "@liveblocks/react-ui";import { useInboxNotifications } from "@liveblocks/react";
function Inbox() { const { inboxNotifications, error, fetchMore, fetchMoreError } = useInboxNotifications();
// Handle error if the initial load failed. // The `error` field is not returned by the Suspense hook as the error is thrown to nearest ErrorBoundary if (error) { return ( <div> <p>Error loading inbox notifications: {error.message}</p> </div> ); }
return ( <div> {inboxNotifications.map((notification) => ( <InboxNotification key={notification.id} inboxNotification={notification} /> ))}
{fetchMoreError && ( <div> <p> Error loading more inbox notifications: {fetchMoreError.message} </p> <button onClick={fetchMore}>Retry</button> </div> )} </div> );}
Batched notifications
If you’re
batching custom notifications,
you can render each activity inside a single notification. The activities
array will contain multiple items.
import { InboxNotification } from "@liveblocks/react-ui";import { useInboxNotifications } from "@liveblocks/react/suspense";
function Inbox() { const { inboxNotifications } = useInboxNotifications();
// If the last notification was batched, it will have multiple // items in the `activities` array // { // id: "in_3dH7sF3...", // kind: "$fileUploaded", // activities: [ // { status: "processing" }, // { status: "complete" }, // ], // ... // } console.log(inboxNotifications[0].activities);
// ...}
useUnreadInboxNotificationsCountLiveblocksProvider
Returns the number of unread inbox notifications for the current user. Suspense and regular versions of this hook are available.
import { useUnreadInboxNotificationsCount } from "@liveblocks/react";
const { count, error, isLoading } = useUnreadInboxNotificationsCount();
Arguments
- optionsUseUnreadInboxNotificationsCountOptions
Optional configuration object.
Options
- queryInboxNotificationsQuery
Optional query to filter notifications count by room ID or kind.
Returns
- countnumber | undefined
The number of unread inbox notifications for the current user, or
undefined if not yet loaded (in non-Suspense version).
- isLoadingboolean
Whether the number of unread inbox notifications is currently being loaded.
- errorError | undefined
Any error that occurred while loading the number of unread inbox notifications.
Querying unread inbox notifications count
It’s possible to return the count of unread inbox notifications that match a
certain query with the query option. You can filter unread inbox notifications
count based on their associated room ID or kind.
// Returns the count that match the entire `query`, e.g. { roomId: "room1", ... }const { count } = useUnreadInboxNotificationsCount({ query: { // Filter for roomId roomId: "room1", // Filter for kind kind: "thread", },});
useMarkInboxNotificationAsReadLiveblocksProvider
Returns a function that marks an inbox notification as read for the current user.
import { useMarkInboxNotificationAsRead } from "@liveblocks/react/suspense";
const markInboxNotificationAsRead = useMarkInboxNotificationAsRead();markInboxNotificationAsRead("in_xxx");
Returns
- markInboxNotificationAsRead(inboxNotificationId: string) => void
A function that marks an inbox notification as read for the current user.
Error handling
useMarkInboxNotificationAsRead marks notifications as read optimistically,
meaning that the notification appears read instantly, before Liveblocks has
confirmed a successful status change. To catch any errors that occur, add
useErrorListener and look for the MARK_INBOX_NOTIFICATION_AS_READ_ERROR
type.
import { useErrorListener } from "@liveblocks/react/suspense";
useErrorListener((error) => { if (error.context.type === "MARK_INBOX_NOTIFICATION_AS_READ_ERROR") { const { inboxNotificationId, roomId } = error.context; console.log(`Problem marking notification as read ${inboxNotificationId}`); }});
useMarkAllInboxNotificationsAsReadLiveblocksProvider
Returns a function that marks all of the current user‘s inbox notifications as read.
import { useMarkAllInboxNotificationsAsRead } from "@liveblocks/react/suspense";
const markAllInboxNotificationsAsRead = useMarkAllInboxNotificationsAsRead();markAllInboxNotificationsAsRead();
Returns
- markAllInboxNotificationsAsRead() => void
A function that marks all of the current user's inbox notifications as read.
Error handling
useMarkAllInboxNotificationsAsRead marks all notifications as read
optimistically, meaning that the notifications appear read instantly, before
Liveblocks has confirmed a successful status change. To catch any errors that
occur, add useErrorListener and look for the
MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR type.
import { useErrorListener } from "@liveblocks/react/suspense";
useErrorListener((error) => { if (error.context.type === "MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR") { console.log("Problem marking all notifications as read"); }});
useDeleteInboxNotificationLiveblocksProvider
Returns a function that deletes an inbox notification for the current user.
import { useDeleteInboxNotification } from "@liveblocks/react/suspense";
const deleteInboxNotification = useDeleteInboxNotification();deleteInboxNotification("in_xxx");
Returns
- deleteInboxNotification(inboxNotificationId: string) => void
A function that deletes an inbox notification for the current user.
Error handling
useDeleteInboxNotification deletes notifications optimistically, meaning that
the notification appears deleted instantly, before Liveblocks has confirmed a
successful deletion. To catch any errors that occur, add useErrorListener
and look for the DELETE_INBOX_NOTIFICATION_ERROR type.
import { useErrorListener } from "@liveblocks/react/suspense";
useErrorListener((error) => { if (error.context.type === "DELETE_INBOX_NOTIFICATION_ERROR") { const { inboxNotificationId } = error.context; console.log(`Problem deleting notification ${inboxNotificationId}`); }});
useDeleteAllInboxNotificationsLiveblocksProvider
Returns a function that deletes all of the current user‘s inbox notifications.
import { useDeleteAllInboxNotifications } from "@liveblocks/react/suspense";
const deleteAllInboxNotifications = useDeleteAllInboxNotifications();deleteAllInboxNotifications();
Returns
- deleteAllInboxNotifications() => void
A function that deletes all of the current user’s inbox notifications.
Error handling
useDeleteAllInboxNotifications deletes all notifications optimistically,
meaning that the notifications appear deleted instantly, before Liveblocks has
confirmed a successful deletion. To catch any errors that occur, add
useErrorListener and look for the DELETE_ALL_INBOX_NOTIFICATIONS_ERROR
type.
import { useErrorListener } from "@liveblocks/react/suspense";
useErrorListener((error) => { if (error.context.type === "DELETE_ALL_INBOX_NOTIFICATIONS_ERROR") { console.log("Problem deleting all notifications"); }});
useInboxNotificationThreadLiveblocksProvider
Returns the thread associated with a "thread" inbox notification.
import { useInboxNotificationThread } from "@liveblocks/react/suspense";
const thread = useInboxNotificationThread("in_xxx");
It can only be called with IDs of "thread" inbox notifications, so we
recommend only using it
when customizing the rendering
or in other situations where you can guarantee the kind of the notification.
No fetching and waterfalls
When useInboxNotifications returns "thread" inbox notifications, it also
receives the associated threads and caches them behind the scenes. When you call
useInboxNotificationThread, it simply returns the cached thread for the inbox
notification ID you passed to it, without any fetching or waterfalls.
Arguments
- inboxNotificationIdstring
The ID of the inbox notification to get the associated thread for. Must be a "thread" type notification.
Returns
- threadThreadData | null
The thread associated with the inbox notification, or null if not found.
useRoomSubscriptionSettingsRoomProvider
Returns the user’s subscription settings for the current room and a function to
update them. Updating this setting will change which
inboxNotifications the current user receives in the
current room.
import { useRoomSubscriptionSettings } from "@liveblocks/react/suspense";
const [{ settings }, updateSettings] = useRoomSubscriptionSettings();
// { threads: "replies_and_mentions", textMentions: "mine" }console.log(settings);
// No longer receive thread subscriptions in this roomupdateSettings({ threads: "none",});
For "threads", these are the three possible values that can be set:
"all"Receive notifications for every activity in every thread."replies_and_mentions"Receive notifications for mentions and threads you’re participating in."none"No notifications are received.
For "textMentions", these are the two possible values that can be set:
"mine"Receive notifications for mentions of you."none"No notifications are received.
Returns
- settingsRoomSubscriptionSettings
The current subscription settings for the room.
- updateSettings(settings: Partial
) => void
A function to update the subscription settings.
useUpdateRoomSubscriptionSettingsRoomProvider
Returns a function that updates the user’s notification settings for the current
room. Updating this setting will change which
inboxNotifications the current user receives in the
current room.
import { useUpdateRoomSubscriptionSettings } from "@liveblocks/react/suspense";
const updateRoomSubscriptionSettings = useUpdateRoomSubscriptionSettings();
// No longer receive thread notifications in this roomupdateSettings({ threads: "none",});
Error handling
useUpdateRoomSubscriptionSettings updates subscription settings
optimistically, meaning that the settings appear updated instantly, before
Liveblocks has confirmed a successful update. To catch any errors that occur,
add useErrorListener and look for the
UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR type.
import { useErrorListener } from "@liveblocks/react/suspense";
useErrorListener((error) => { if (error.context.type === "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR") { const { roomId } = error.context; console.log(`Problem updating subscription settings for room ${roomId}`); }});
For "threads", these are the three possible values that can be set:
"all"Receive notifications for every activity in every thread."replies_and_mentions"Receive notifications for mentions and threads you're participating in."none"No notifications are received.
For "textMentions", these are the two possible values that can be set:
"mine"Receive notifications for mentions of you."none"No notifications are received.
Works the same as updateSettings in
useRoomSubscriptionSettings.
Returns
- updateRoomSubscriptionSettings(settings: Partial
) => void
A function that updates the user's notification settings for the current room.
Replacing individual thread subscriptions
Subscribing will replace any existing thread subscriptions in the current room. This value can also be overridden by a room-level call that is run afterwards.
const subscribeToThread = useSubscribeToThread();const [{ settings }, updateSettings] = useRoomSubscriptionSettings();
// 1. Enables notifications just for this thread, "th_d75sF3..."subscribeToThread("th_d75sF3...");
// 2. Disables notifications for all threads, including "th_d75sF3..."updateSettings({ threads: "none",});
Error handling
useNotificationSettingsLiveblocksProvider
Notification settings is currently in beta.
Returns the user’s notification settings in the current project, in other words
which notification webhook events
will be sent for the current user. Notification settings are project-based,
which means that settings and updateSettings are for the current user’s
settings in every room. Useful for creating a
notification settings panel.
import { useNotificationSettings } from "@liveblocks/react";
const [{ isLoading, error, settings }, updateSettings] = useNotificationSettings();
// Current user receives thread notifications on the email channel// { email: { thread: true, ... }, ... }console.log(settings);
// Disabling thread notifications on the email channelupdateSettings({ email: { thread: false, },});
A user’s initial settings are set in the dashboard, and different kinds should
be enabled there. If no kind is enabled on the current channel, null will be
returned. For example, with the email channel:
const [{ isLoading, error, settings }, updateSettings] = useNotificationSettings();
// { email: null, ... }console.log(settings);
Updating notification settings
The updateSettings function can be used to update the current user’s
notification settings, changing their settings for every room in the project.
Each notification kind must first be enabled on your project’s notification
dashboard page before settings can be used.
Suspense and
regular versions of this
hook are available.
// You only need to pass partialsupdateSettings({ email: { thread: true },});
// Enabling a custom notification on the slack channelupdateSettings({ slack: { $myCustomNotification: true },});
// Setting complex settingsupdateSettings({ email: { thread: true, textMention: false, $newDocument: true, }, slack: { thread: false, $fileUpload: false, }, teams: { thread: true, },});
Returns
- settingsNotificationSettings | null
The current notification settings for the user, or null if no settings are configured.
- updateSettings(settings: Partial
) => void
A function to update the notification settings.
- isLoadingboolean
Whether the notification settings are currently being loaded.
- errorError | null
Any error that occurred while loading the notification settings.
Error handling
Error handling is an important aspect to consider when using the
useNotificationSettings hook. The error fields provides information about
any error that occurred during the fetch operation.
The following example shows how to display error messages for both initial loading errors and errors that occur when fetching more inbox notifications.
import { useNotificationSettings } from "@liveblocks/react";
const [{ isLoading, error, settings }, updateSettings] = useNotificationSettings();
if (error) { return ( <div> <p>Error loading notification settings: {error.message}</p> </div> );}
useUpdateNotificationSettingsLiveblocksProvider
Notification settings is currently in beta.
Returns a function that updates user’s notification settings, which affects
which notification webhook events
will be sent for the current user. Notification settings are project-based,
which means that updateSettings modifies the current user’s settings in every
room. Each notification kind must first be enabled on your project’s
notification dashboard page before settings can be used. Useful for creating a
notification settings panel.
import { useUpdateNotificationSettings } from "@liveblocks/react";
const updateSettings = useUpdateNotificationSettings();
// Disabling thread notifications on the email channelupdateSettings({ email: { thread: false },});
Works the same as updateSettings in
useNotificationSettings. You can pass a partial
object, or many settings at once.
Returns
- updateNotificationSettings(settings: Partial
) => void
A function that updates the user's notification settings for the current project.
Version History
useHistoryVersionsRoomProvider
Returns the versions of the room. See Version History Components for more information on how to display versions.
import { useHistoryVersions } from "@liveblocks/react";
const { versions, error, isLoading } = useHistoryVersions();
Returns
- versionsHistoryVersion[]
An array of history versions for the room.
Miscellaneous
useUserBoth
Returns user info from a given user ID. To use useUser, you should provide a
resolver function to the resolveUsers option in createClient or
LiveblocksProvider.
Suspense and
regular versions of this
hook are available.
import { useUser } from "@liveblocks/react";
const { user, error, isLoading } = useUser("user-id");
Arguments
- userIdstring
The ID of the user to get information for.
Returns
- userUser | undefined
The user information, or undefined if not found or not yet loaded (in
non-Suspense version).
- isLoadingboolean
Whether the user information is currently being loaded.
- errorError | undefined
Any error that occurred while loading the user information.
useRoomInfoBoth
Returns room info from a given room ID. To use useRoomInfo, you should provide
a resolver function to the resolveRoomsInfo option in createClient
or LiveblocksProvider.
Suspense and
regular versions of this
hook are available.
import { useRoomInfo } from "@liveblocks/react";
const { info, error, isLoading } = useRoomInfo("room-id");
Arguments
- roomIdstring
The ID of the room to get information for.
Returns
- infoRoomInfo | undefined
The room information, or undefined if not found or not yet loaded (in
non-Suspense version).
- isLoadingboolean
Whether the room information is currently being loaded.
- errorError | undefined
Any error that occurred while loading the room information.
useGroupInfoBoth
Returns group info from a given group ID. To use useGroupInfo, you should
provide a resolver function to the [resolveGroupsInfo][] option in
createClient or LiveblocksProvider.
Suspense and
regular versions of this
hook are available.
import { useGroupInfo } from "@liveblocks/react";
const { info, error, isLoading } = useGroupInfo("group-id");
Arguments
- groupIdstring
The ID of the group to get information for.
Returns
- infoGroupInfo | undefined
The group information, or undefined if not found or not yet loaded (in
non-Suspense version).
- isLoadingboolean
Whether the group information is currently being loaded.
- errorError | undefined
Any error that occurred while loading the group information.
useUrlMetadataBoth
Returns metadata for a given URL. Suspense and regular versions of this hook are available.
import { useUrlMetadata } from "@liveblocks/react";
const { metadata, error, isLoading } = useUrlMetadata("https://liveblocks.io");
// metadata.title, metadata.description, metadata.image, metadata.icon, ...
Arguments
- urlstring
The URL to get metadata for.
Returns
- metadataUrlMetadata | undefined
The metadata for the URL, or undefined if not yet loaded (in non-Suspense
version).
- isLoadingboolean
Whether the metadata is currently loading.
- errorError | undefined
Any error that occurred while loading the metadata.
TypeScript
Typing your data
It’s possible to have automatic types flow through your application by defining
a global Liveblocks interface. We recommend doing this in a
liveblocks.config.ts file in the root of your app, so it’s easy to keep track
of your types. Each type (Presence, Storage, etc.), is optional, but it’s
recommended to make use of them.
liveblocks.config.ts
declare global { interface Liveblocks { // Each user's Presence, for useMyPresence, useOthers, etc. Presence: {};
// The Storage tree for the room, for useMutation, useStorage, etc. Storage: {};
UserMeta: { id: string; // Custom user info set when authenticating with a secret key info: {}; };
// Custom events, for useBroadcastEvent, useEventListener RoomEvent: {};
// Custom metadata set on threads, for useThreads, useCreateThread, etc. ThreadMetadata: {};
// Custom room info set with resolveRoomsInfo, for useRoomInfo RoomInfo: {};
// Custom group info set with resolveGroupsInfo, for useGroupInfo GroupInfo: {};
// Custom activities data for custom notification kinds ActivitiesData: {}; }}
// Necessary if you have no imports/exportsexport {};
Here are some example values that might be used.
liveblocks.config.ts
import { LiveList } from "@liveblocks/client";
declare global { interface Liveblocks { // Each user's Presence, for useMyPresence, useOthers, etc. Presence: { // Example, real-time cursor coordinates cursor: { x: number; y: number }; };
// The Storage tree for the room, for useMutation, useStorage, etc. Storage: { // Example, a conflict-free list animals: LiveList<string>; };
UserMeta: { id: string; // Custom user info set when authenticating with a secret key info: { // Example properties, for useSelf, useUser, useOthers, etc. name: string; avatar: string; }; };
// Custom events, for useBroadcastEvent, useEventListener // Example has two events, using a union RoomEvent: { type: "PLAY" } | { type: "REACTION"; emoji: "🔥" };
// Custom metadata set on threads, for useThreads, useCreateThread, etc. ThreadMetadata: { // Example, attaching coordinates to a thread x: number; y: number; };
// Custom room info set with resolveRoomsInfo, for useRoomInfo RoomInfo: { // Example, rooms with a title and url title: string; url: string; };
// Custom group info set with resolveGroupsInfo, for useGroupInfo GroupInfo: { // Example, groups with a name and a badge name: string; badge: string; };
// Custom activities data for custom notification kinds ActivitiesData: { // Example, a custom $alert kind $alert: { title: string; message: string; }; }; }}
// Necessary if you have no imports/exportsexport {};
Typing with createRoomContext
Before Liveblocks 2.0, it was recommended to create your hooks using
createRoomContext, and manually pass your types to this function. This is
no longer the recommended method for setting up Liveblocks,
but it can still be helpful, for example you can use createRoomContext
multiple times to create different room types, each with their own correctly
typed hooks.
liveblocks.config.ts
To upgrade to Liveblocks 2.0 and the new typing system, follow the 2.0 migration guide.
User
User is a type that’s returned by useSelf, useOthers, and other
functions. Some of its values are set when
typing your room, here are some example values:
liveblocks.config.ts
declare global { interface Liveblocks { // Each user’s Presence Presence: { cursor: { x: number; y: number }; };
UserMeta: { id: string; // Custom user info set when authenticating with a secret key info: { name: string; avatar: string; }; }; }}
const { room, leave } = client.enterRoom("my-room-id");
// {// connectionId: 52,// presence: {// cursor: { x: 263, y: 786 },// },// id: "mislav.abha@example.com",// info: {// name: "Mislav Abha",// avatar: "/mislav.png",// },// canWrite: true,// canComment: true,// }const user = room.getSelf();
Properties
- connectionIdnumber
The connection ID of the User. It is unique and increments with every new connection.
- idUserMeta["id"]
The ID of the User that has been set in the authentication endpoint. Useful to get additional information about the connected user.
- infoUserMeta["info"]
Additional user information that has been set in the authentication endpoint.
- presenceTPresence
The user’s Presence data.
- canWriteboolean
true if the user can mutate the Room’s Storage and/or YDoc, false if
they can only read but not mutate it. Set via your room\
permissions.
- canCommentboolean
true if the user can leave a comment in the room, false if they can only
read comments but not leave them. Set via your room\
permissions.
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);
Examples are illustrated via useStorage
In this section, useStorage is used as the canonical example. This is for
illustration purposes only. The described concepts and behaviors apply equally
to the other selector hooks.
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 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 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.animalsinitially is["🦁", "🦊", "🐵"]. - Then, something unrelated elsewhere in Storage is changed. In response to the
change,
root.animalsgets 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.animalsgets re-evaluated, and now it returns["🦁", "🦊"]. - Because the previous value and this value are different, the component will rerender, seeing the updated value.