---
meta:
  title: "@liveblocks/react"
  parentTitle: "API Reference"
  description: "API Reference for the @liveblocks/react package"
alwaysShowAllNavigationLevels: false
---

`@liveblocks/react` provides you with [React](https://react.dev/) bindings for
our realtime collaboration APIs, built on top of WebSockets. Read our
[getting started](/docs/get-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](https://react.dev/reference/react/Suspense), and those that
don’t.

```tsx
// Import the Suspense hook
import { useThreads } from "@/liveblocks/react/suspense";

// Import the regular hook
import { 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.

<Banner title="Enable Suspense with createRoomContext">

If you’re using the non-standard [`createRoomContext`](#createRoomContext)
function to build your hooks, you must
[enable suspense differently](#createRoomContext-Suspense).

</Banner>

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

```tsx
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`](https://github.com/bvaughn/react-error-boundary) to render an
error if the hook runs into a problem.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider
    // publicApiKey=""
    // authEndpoint="/api/liveblocks-auth"
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

```tsx title="All LiveblocksProvider props" isCollapsable isCollapsed
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider
      // Connect with authEndpoint
      authEndpoint="/api/liveblocks-auth"
      // ---
      // Alternatively, use an authEndpoint callback
      // authEndpoint={async (room) => {
      //   const response = await fetch("/api/liveblocks-auth", {
      //     method: "POST",
      //     headers: {
      //       Authentication: "<your own headers here>",
      //       "Content-Type": "application/json",
      //     },
      //     body: JSON.stringify({ room }),
      //   });
      //   return await response.json();
      // }}
      //
      // Alternatively, use a public key
      // publicApiKey="pk_..."
      //
      // Throttle time (ms) between WebSocket updates
      throttle={100}
      // ---
      // Prevent browser tab from closing while local changes aren’t synchronized yet
      preventUnsavedChanges={false}
      // ---
      // Throw lost-connection event after 5 seconds offline
      lostConnectionTimeout={5000}
      // ---
      // Disconnect users after X (ms) of inactivity, disabled by default
      backgroundKeepAliveTimeout={undefined}
      // ---
      // Resolve user info for Comments, Text Editor, and Notifications
      resolveUsers={async ({ userIds }) => {
        const usersData = await __getUsersFromDB__(userIds);

        return usersData.map((userData) => ({
          name: userData.name,
          avatar: userData.avatar.src,
        }));
      }}
      // ---
      // Resolve room info for Notifications
      resolveRoomsInfo={async ({ roomIds }) => {
        const documentsData = await __getDocumentsFromDB__(roomIds);

        return documentsData.map((documentData) => ({
          name: documentData.name,
          // url: documentData.url,
        }));
      }}
      // ---
      // Resolve group info for Comments and Text Editor
      resolveGroupsInfo={async ({ groupIds }) => {
        const groupsData = await __getGroupsFromDB__(groupIds);

        return groupsData.map((groupData) => ({
          avatar: groupData.avatar.src,
          name: groupData.name,
          // description: groupData.description,
        }));
      }}
      // ---
      // Resolve mention suggestions for Comments and Text Editor
      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);
        }
      }}
      // ---
      // Polyfill options for non-browser environments
      polyfills={
        {
          // atob,
          // fetch,
          // WebSocket,
        }
      }
      // ---
      // Set the location of the "Powered by Liveblocks" badge
      // "top-right", "bottom-right", "bottom-left", "top-left"
      badgeLocation="bottom-right"
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

<PropertiesList title="Props">
  <PropertiesListItem
    name="authEndpoint"
    detailedType="string | async ((room?: string) => CustomAuthenticationResult)"
  >
    The URL of your back end’s [authentication endpoint](/docs/authentication)
    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](#LiveblocksProviderAuthEndpoint) and [using a
    callback](#LiveblocksProviderCallback).
  </PropertiesListItem>
  <PropertiesListItem name="publicApiKey" type="string">
    The public API key taken from your project’s
    [dashboard](/dashboard/apikeys). Generally not recommended for production
    use. Either `authEndpoint` or `publicApiKey` are required. [Learn
    more](#LiveblocksProviderPublicKey).
  </PropertiesListItem>
  <PropertiesListItem name="throttle" type="number" defaultValue="100">
    The throttle time between WebSocket messages in milliseconds, a number
    between `16` and `1000` is allowed. Using `16` means your app will update 60
    times per second. [Learn more](#LiveblocksProviderThrottle).
  </PropertiesListItem>
  <PropertiesListItem
    name="preventUnsavedChanges"
    type="boolean"
    defaultValue="false"
  >
    When set, navigating away from the current page is prevented while
    Liveblocks is still synchronizing local changes. [Learn
    more](#prevent-users-losing-unsaved-changes).
  </PropertiesListItem>
  <PropertiesListItem
    name="lostConnectionTimeout"
    type="number"
    defaultValue="5000"
  >
    After a user disconnects, the time in milliseconds before a
    [`"lost-connection"`](/docs/api-reference/liveblocks-client#Room.subscribe.lost-connection)
    event is fired. [Learn more](#LiveblocksProviderLostConnectionTimeout).
  </PropertiesListItem>
  <PropertiesListItem name="backgroundKeepAliveTimeout" type="number">
    The time before an inactive WebSocket connection is disconnected. This is
    disabled by default, but setting a number will activate it. [Learn
    more](#LiveblocksProviderBackgroundKeepAliveTimeout).
  </PropertiesListItem>
  <PropertiesListItem
    name="resolveUsers"
    detailedType='async? (args: ResolveUsersArgs) => (UserMeta["info"] | undefined)[] | undefined'
  >
    A function that resolves user information in
    [Comments](/docs/ready-made-features/comments), [Text
    Editor](/docs/ready-made-features/text-editor), and
    [Notifications](/docs/ready-made-features/notifications). Return an array of
    `UserMeta["info"]` objects in the same order they arrived. [Learn
    more](#LiveblocksProviderResolveUsers).
  </PropertiesListItem>
  <PropertiesListItem
    name="resolveRoomsInfo"
    detailedType="async? (args: ResolveRoomsInfoArgs) => (RoomInfo | undefined)[] | undefined"
  >
    A function that resolves room information in
    [Notifications](/docs/ready-made-features/notifications). Return an array of
    `RoomInfo` objects in the same order they arrived. [Learn
    more](#LiveblocksProviderResolveRoomsInfo).
  </PropertiesListItem>
  <PropertiesListItem
    name="resolveGroupsInfo"
    detailedType="async? (args: ResolveGroupsInfoArgs) => (GroupInfo | undefined)[] | undefined"
  >
    A function that resolves group information in
    [Comments](/docs/ready-made-features/comments) and [Text
    Editor](/docs/ready-made-features/text-editor). Return an array of
    `GroupInfo` objects in the same order they arrived. [Learn
    more](#LiveblocksProviderResolveGroupsInfo).
  </PropertiesListItem>
  <PropertiesListItem
    name="resolveMentionSuggestions"
    detailedType="async? (args: ResolveMentionSuggestionsArgs) => string[] | MentionData[]"
  >
    A function that resolves mention suggestions in
    [Comments](/docs/ready-made-features/comments) and [Text
    Editor](/docs/ready-made-features/text-editor). Return an array of user IDs
    or mention objects. [Learn
    more](#LiveblocksProviderResolveMentionSuggestions).
  </PropertiesListItem>
  <PropertiesListItem name="polyfills">
    Place polyfills for `atob`, `fetch`, and `WebSocket` inside here. Useful
    when using a non-browser environment, such as
    [Node.js](#LiveblocksProviderNode) or [React
    Native](#LiveblocksProviderReactNative).
  </PropertiesListItem>
  <PropertiesListItem
    name="badgeLocation"
    detailedtype='"top-right" | "bottom-right" | "bottom-left" | "top-left"'
    defaultValue={`"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](#Powered-by-Liveblocks-branding).
  </PropertiesListItem>
</PropertiesList>

#### LiveblocksProvider with public key [#LiveblocksProviderPublicKey]

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](#LiveblocksProviderAuthEndpoint).

```tsx
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
      {/* children */}
    </LiveblocksProvider>
  );
}
```

#### LiveblocksProvider with auth endpoint [#LiveblocksProviderAuthEndpoint]

If you are not using a public key, you need to set up your own `authEndpoint`.
Please refer to our [Authentication guide](/docs/authentication).

```tsx
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider authEndpoint="/api/liveblocks-auth">
      {/* children */}
    </LiveblocksProvider>
  );
}
```

<Banner title="Don't cache tokens" type="warning">

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.

</Banner>

#### LiveblocksProvider with auth endpoint callback [#LiveblocksProviderCallback]

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`](/docs/api-reference/liveblocks-node#access-tokens)
or [`liveblocks.identifyUser`](/docs/api-reference/liveblocks-node#id-tokens),
learn more in [authentication guide](/docs/rooms/authentication).

```tsx
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](/docs/ready-made-features/comments/email-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.

```tsx
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`](/docs/api-reference/liveblocks-node#access-tokens)
or [`liveblocks.identifyUser`](/docs/api-reference/liveblocks-node#id-tokens).
These are the values the functions can return.

1. A valid token, it returns a `{ "token": "..." }` shaped response.
1. 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 [#LiveblocksProviderThrottle]

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.

```tsx
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.

```ts
throttle:  16, // 60 FPS
throttle:  32, // 30 FPS
throttle: 200, //  5 FPS
```

#### Prevent users losing unsaved changes [#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.

```tsx
function Page() {
  return (
    <LiveblocksProvider
      // highlight-next-line
      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](/docs/ready-made-features/text-editor).
- There’s an unsubmitted comment in the
  [Composer](/docs/api-reference/liveblocks-react-ui#Composer).
- The user has made changes and is currently offline.

Internally, this option uses the
[beforeunload event](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event).

#### Lost connection timeout [#LiveblocksProviderLostConnectionTimeout]

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.

```tsx
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 [#LiveblocksProviderBackgroundKeepAliveTimeout]

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.

```tsx
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 [#LiveblocksProviderResolveUsers]

[Comments](/docs/ready-made-features/comments) and
[Text Editor](/docs/ready-made-features/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.

```tsx
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`](/docs/api-reference/liveblocks-react-ui#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](/docs/api-reference/liveblocks-react-ui#Components),
they are necessary. Here’s an example of `userIds` and the exact values
returned.

```tsx
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`:

```tsx
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`](/docs/api-reference/liveblocks-react#useUser) hook.

```tsx
import { useUser } from "@liveblocks/react/suspense";

function Component() {
  const user = useUser("marc@example.com");

  // { name: "Marc", avatar: "https://...", ... }
  console.log(user);
}
```

#### resolveRoomsInfo [#LiveblocksProviderResolveRoomsInfo]

When using
[Notifications](/docs/ready-made-features/comments/email-notifications) with
[Comments](/docs/ready-made-features/comments), room IDs will be used to
contextualize notifications (e.g. “Chris mentioned you in _room-id_”) in the
[`InboxNotification`](/docs/api-reference/liveblocks-react-ui#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`](#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.

```tsx
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`](/docs/api-reference/liveblocks-react-ui#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`](/docs/api-reference/liveblocks-react-ui#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 [#LiveblocksProviderResolveGroupsInfo]

When using group mentions with [Comments](/docs/ready-made-features/comments)
and [Text Editor](/docs/ready-made-features/text-editor), group IDs will be used
instead of user IDs. Similarly to
[`resolveUsers`](#LiveblocksProviderResolveUsers), you can provide a resolver
function to the `resolveGroupsInfo` option in
[`LiveblocksProvider`](#LiveblocksProvider) to assign information like names and
avatars to group IDs.

```tsx
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`](/docs/api-reference/liveblocks-react#useGroupInfo) hook.

```tsx
import { useGroupInfo } from "@liveblocks/react/suspense";

function Component() {
  const group = useGroupInfo("group-engineering");

  // { name: "Engineering", avatar: "https://...", ... }
  console.log(group);
}
```

#### resolveMentionSuggestions [#LiveblocksProviderResolveMentionSuggestions]

To enable creating mentions in [Comments](/docs/ready-made-features/comments)
and [Text Editor](/docs/ready-made-features/text-editor), you can provide a
resolver function to the `resolveMentionSuggestions` option in
[`LiveblocksProvider`](#LiveblocksProvider). These mentions will be displayed in
the [`Composer`](/docs/api-reference/liveblocks-react-ui#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.

```tsx
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](/docs/ready-made-features/comments) and
[Text Editor](/docs/ready-made-features/text-editor), you can return a list of
mention objects instead of user IDs to suggest a mix of user and group mentions.

```tsx
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.

```tsx
// 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 [#LiveblocksProviderNode]

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

```bash
npm install ws node-fetch
```

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

```tsx
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](https://github.com/node-fetch/node-fetch/blob/main/docs/v3-UPGRADE-GUIDE.md#converted-to-es-module).
If you are using CommonJS, downgrade `node-fetch` to v2.

#### LiveblocksProvider for React Native [#LiveblocksProviderReactNative]

To use `@liveblocks/client` with [React Native](https://reactnative.dev/), you
need to add an [`atob`][] polyfill. As a polyfill, we recommend installing
[`base-64`][].

```bash
npm install base-64
```

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

```ts
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`.

```tsx title="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:

1. In the Liveblocks dashboard, navigate to your
   [team’s settings](/dashboard/settings).
2. Under **General**, toggle on the remove "Powered by Liveblocks" branding
   option.

<Banner title='Removing the "Powered by Liveblocks" badge'>

Removing the "Powered by Liveblocks" badge on your projects requires a
[paid plan](/pricing/). See the [pricing page](/pricing/) for more information.

</Banner>

### createLiveblocksContext

<Banner title="Not recommended" type="warning">

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](#Typing-your-data),
unless you need to define multiple room types in your application.

</Banner>

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`](#createRoomContext) offers APIs for interacting with
rooms (e.g. Presence, Storage, and Comments),
[`createLiveblocksContext`](#createLiveblocksContext) offers APIs for
interacting with Liveblocks features that are not tied to a specific room (e.g.
Notifications).

```tsx file="liveblocks.config.ts"
import { createClient } from "@liveblocks/client";
import { createRoomContext, createLiveblocksContext } from "@liveblocks/react";

const client = createClient({
  // publicApiKey: "",
  // authEndpoint: "/api/liveblocks-auth",
  // throttle: 100,
});

// ...

export const { RoomProvider } = createRoomContext(client);

export const {
  LiveblocksProvider,
  useInboxNotifications,

  // Other hooks
  // ...
} = createLiveblocksContext(client);
```

### useClient [@badge=LiveblocksProvider]

Returns the [`client`](/docs/api-reference/liveblocks-client#createClient) of
the nearest [`LiveblocksProvider`][] above in the React component tree.

```ts
import { useClient } from "@liveblocks/react/suspense";

const client = useClient();
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem name="client" type="Client">
    The [Liveblocks client](/docs/api-reference/liveblocks-client#createClient)
    instance from the nearest [`LiveblocksProvider`][].
  </PropertiesListItem>
</PropertiesList>

### useErrorListener [@badge=LiveblocksProvider]

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

```ts
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.

```ts title="All error types" isCollapsable isCollapsed
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  switch (error.context.type) {
    // Can happen if you use Presence, Storage, or Yjs
    case "ROOM_CONNECTION_ERROR": {
      const { code } = error.context;
      // -1   = Authentication error
      // 4001 = You don't have access to this room
      // 4005 = Room was full
      // 4006 = Room ID has changed
      break;
    }

    // Can happen if you use Comments or Notifications
    case "CREATE_THREAD_ERROR":
      const { roomId, threadId, commentId, body, metadata } = error.context;
      break;

    case "DELETE_THREAD_ERROR":
      const { roomId, threadId } = error.context;
      break;

    case "EDIT_THREAD_METADATA_ERROR":
      const { roomId, threadId, metadata } = error.context;
      break;

    case "MARK_THREAD_AS_RESOLVED_ERROR":
    case "MARK_THREAD_AS_UNRESOLVED_ERROR":
      const { roomId, threadId } = error.context;
      break;

    case "CREATE_COMMENT_ERROR":
    case "EDIT_COMMENT_ERROR":
      const { roomId, threadId, commentId, body } = error.context;
      break;

    case "DELETE_COMMENT_ERROR":
      const { roomId, threadId, commentId } = error.context;
      break;

    case "ADD_REACTION_ERROR":
    case "REMOVE_REACTION_ERROR":
      const { roomId, threadId, commentId, emoji } = error.context;
      break;

    case "MARK_INBOX_NOTIFICATION_AS_READ_ERROR":
      const { inboxNotificationId, roomId } = error.context;
      break;

    case "DELETE_INBOX_NOTIFICATION_ERROR":
      const { roomId } = error.context;
      break;

    case "MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR":
    case "DELETE_ALL_INBOX_NOTIFICATIONS_ERROR":
      break;

    case "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR":
      const { roomId } = error.context;
      break;

    default:
      // Ignore any error from the future
      break;
  }
});
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="callback" type="(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.
  </PropertiesListItem>
</PropertiesList>

## AI Copilots

### useAiChats [@badge=LiveblocksProvider]

Returns a paginated list of AI chats created by the current user. Initially
fetches the latest 50 chats.
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useAiChats } from "@liveblocks/react";

const { chats, error, isLoading } = useAiChats();
```

<PropertiesList title="Options">
  <PropertiesListItem name="query" type="AiChatsQuery">
    Optional query to filter chats by metadata values or absence of metadata
    keys. [Learn more](/docs/api-reference/liveblocks-react#useAiChats-query)
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="chats" type="AiChat[] | undefined">
    An array of AI chats created by the current user.
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the chats are currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the chats. [Learn
    more](/docs/api-reference/liveblocks-react#useAiChats-error-handling).
  </PropertiesListItem>
  <PropertiesListItem name="hasFetchedAll" type="boolean">
    Whether all available chats have been fetched. [Learn
    more](/docs/api-reference/liveblocks-react#useAiChats-pagination).
  </PropertiesListItem>
  <PropertiesListItem name="fetchMore" type="() => void">
    A function to fetch more chats. [Learn
    more](/docs/api-reference/liveblocks-react#useAiChats-pagination).
  </PropertiesListItem>
  <PropertiesListItem name="isFetchingMore" type="boolean">
    Whether more chats are currently being fetched. [Learn
    more](/docs/api-reference/liveblocks-react#useAiChats-pagination).
  </PropertiesListItem>
  <PropertiesListItem name="fetchMoreError" type="Error | undefined">
    Any error that occurred while fetching more chats. [Learn
    more](/docs/api-reference/liveblocks-react#useAiChats-pagination).
  </PropertiesListItem>
</PropertiesList>

##### List the user's chats and switch between them

You can use the [`AiChat`](/docs/api-reference/liveblocks-react-ui#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`](#useDeleteAiChat).

```tsx
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 [#useAiChats-query]

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.

```tsx
import { useAiChats } from "@liveblocks/react";

// Filter by metadata values and by absence of a key
const { 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 [#useAiChats-pagination]

By default, the `useAiChats` hook returns up to 50 chats. To fetch more, the
hook provides additional fields for pagination, similar to [`useThreads`][].

```tsx
import { useAiChats } from "@liveblocks/react";

const {
  chats,
  isLoading,
  error,

  +++
  hasFetchedAll,
  fetchMore,
  isFetchingMore,
  fetchMoreError,
  +++
} = useAiChats();
```

- `hasFetchedAll` indicates whether all available AI chats have been fetched.
- `fetchMore` loads up to 50 more AI chats, and is always safe to call.
- `isFetchingMore` indicates whether more AI chats are being fetched.
- `fetchMoreError` returns error statuses resulting from fetching more.

##### Pagination example [#useAiChats-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.

```tsx
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 [#useAiChats-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.

```tsx
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](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useAiChat } from "@liveblocks/react";

const { chat, error, isLoading } = useAiChat("my-chat-id");
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="chatId" type="string">
    The ID of the AI chat to retrieve information for.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="chat" type="AiChat | undefined">
    The AI chat object containing title, metadata, and other properties.
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the chat information is currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the chat information.
  </PropertiesListItem>
</PropertiesList>

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

```tsx
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.

```tsx
import { useCreateAiChat } from "@liveblocks/react/suspense";

const createAiChat = useCreateAiChat();
createAiChat("my-ai-chat");
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem
    name="createAiChat"
    type="(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`.
  </PropertiesListItem>
</PropertiesList>

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

```tsx
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`](#useAiChats) to
[loop through each chat](/docs/api-reference/liveblocks-react#List-the-user's-chats-and-switch-between-them)
and add a delete button.

```tsx
import { useDeleteAiChat } from "@liveblocks/react/suspense";

const deleteAiChat = useDeleteAiChat();
deleteAiChat("my-chat-id");
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem name="deleteAiChat" type="(chatId: string) => void">
    A function that deletes an AI chat by its ID.
  </PropertiesListItem>
</PropertiesList>

### useSendAiMessage

Returns a function that sends a message to an AI chat, identified by its ID.
Useful for
[creating suggestions in empty inside chats](/docs/api-reference/liveblocks-react-ui#AiChat-placeholder)
and sending messages on behalf of the user.

```tsx
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.

```tsx
const sendAiMessage = useSendAiMessage("my-chat-id", {
  // +++
  copilotId: "co_h7GBa3...",
  // +++
});
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="chatId" type="string">
    Optional. The ID of the AI chat to send messages to. Can also be provided
    when calling the returned function.
  </PropertiesListItem>
  <PropertiesListItem name="options" type="object">
    Optional configuration object.
  </PropertiesListItem>
  <PropertiesListItem name="options.copilotId" type="string">
    Optional. The ID of the copilot to use for sending the message.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem
    name="sendAiMessage"
    type="(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`.
  </PropertiesListItem>
</PropertiesList>

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

```tsx
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 message
sendAiMessage({
  text: "Hello world",
  // +++
  copilotId: "co_Xpksa9...",
  // +++
});

// Overwrites the `chatId` just for this message
sendAiMessage({
  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.

```tsx
// +++
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.

```tsx
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.

```tsx
import { useAiChatMessages } from "@liveblocks/react";

const { messages, error, isLoading } = useAiChatMessages("my-chat-id");
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="chatId" type="string">
    The ID of the AI chat to retrieve messages from.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="messages" type="AiChatMessage[] | undefined">
    An array of messages in the AI chat.

    <div className="-mt-1">
    ```tsx isCollapsable isCollapsed title="Example messages"
    [
      {
        "id": "ms_gw1wENvliU471QuojwVbO",
        "chatId": "my-chat-id",
        "parentId": null,
        "createdAt": "2025-09-30T15:37:01.000Z",
        "role": "user",
        "content": [
          {
            "type": "text",
            "text": "Hello"
          }
        ],
        "navigation": {
          "parent": null,
          "prev": null,
          "next": null
        }
      },
      {
        "id": "ms_dwoLgXARKgJT4zAjkktv6",
        "chatId": "my-chat-id",
        "parentId": "ms_gw1wENvliU471QuojwVbO",
        "createdAt": "2025-09-30T15:37:01.000Z",
        "copilotId": "co_lxYkxUdt08d01sJIBUZhg",
        "role": "assistant",
        "status": "completed",
        "content": [
          {
            "type": "text",
            "text": "Hi there! How can I help you today?\n",
            "t": 2783
          }
        ],
        "navigation": {
          "parent": "ms_gw1wENvliU471QuojwVbO",
          "prev": null,
          "next": null
        }
      }
    ]
    ```
    </div>

  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the messages are currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the messages.
  </PropertiesListItem>
</PropertiesList>

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

```tsx
import { useAiChatStatus } from "@liveblocks/react";

const { status, partType, toolName } = useAiChatStatus("my-chat-id");
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="chatId" type="string">
    The ID of the AI chat.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem
    name="status"
    type={`"disconnected" | "loading" | "idle" | "generating"`}
  >
    The current synchronization status of the chat. When disconnected the AI
    service is not available.
  </PropertiesListItem>
  <PropertiesListItem name="partType" type={`"text" | "tool-invocation"`}>
    The type of content being generated.
  </PropertiesListItem>
  <PropertiesListItem name="toolName" type="string | undefined">
    The name of the tool being invoked. If no tool is currently being called,
    returns `undefined`.
  </PropertiesListItem>
</PropertiesList>

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

```tsx
<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`](/docs/api-reference/liveblocks-react#LiveblocksProvider).

```tsx
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 [#RegisterAiKnowledge-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.

```tsx
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 [#RegisterAiKnowledge-user]

You can pass in knowledge from your auth provider, for example with
[`useUser`](https://clerk.com/docs/hooks/use-user) from
[Clerk](https://clerk.com). You can tell AI that the state is loading in a
simple string, and it will understand.

```tsx
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 [#RegisterAiKnowledge-fetching]

You can pass in knowledge from data fetching hooks such as with
[`useSWR`](https://swr.vercel.app/docs/getting-started) from
[SWR](https://swr.vercel.app/). You can tell AI that the state is loading in a
simple string, and it will understand.

```tsx
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 [#RegisterAiKnowledge-text-editor]

You can pass in knowledge from your text editor, for example when using
[Liveblocks Tiptap](/docs/api-reference/liveblocks-react-tiptap).

```tsx
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.

{/* TODO how to make tools where you can write to the text editor */}

#### Pass in comment data [#RegisterAiKnowledge-storage]

You can also pass in knowledge from your custom Liveblocks Comments app with
[`useThreads`](/docs/api-reference/liveblocks-react#useThreads). This way, your
AI chat will understand the context in the current room.

```tsx
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 [#RegisterAiKnowledge-storage]

You can also pass in knowledge from your custom Liveblocks storage app with
[`useStorage`](/docs/api-reference/liveblocks-react#useStorage). This way, your
AI chat will understand the context in the current room.

```tsx
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 [#RegisterAiKnowledge-props]

<PropertiesList>
  <PropertiesListItem name="description" type="string" required>
    A clear description of what this knowledge represents. This helps the AI
    understand the context and relevance of the provided information.
  </PropertiesListItem>
  <PropertiesListItem name="value" type="Json" required>
    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.
  </PropertiesListItem>
  <PropertiesListItem name="id" type="string">
    Optional unique identifier for this knowledge source. If provided,
    subsequent updates with the same ID will replace the previous knowledge.
  </PropertiesListItem>
  <PropertiesListItem name="chatId" type="string">
    Optional chat ID to scope this tool to a specific chat. If provided, the
    tool will only be available to that chat.
  </PropertiesListItem>
</PropertiesList>

### RegisterAiTool

Registers a tool that can be used by
[AI chats](/docs/api-reference/liveblocks-react-ui#AiChat) 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.

```tsx
import { RegisterAiTool } from "@liveblocks/react";
import { defineAiTool } from "@liveblocks/client";

<RegisterAiTool
  name="my-tool"
  tool={defineAiTool()({
    // Tool definition
    // ...
  })}
/>;
```

{/* TODO lots more info */}

[`defineAiTool`](/docs/api-reference/liveblocks-client#defineAiTool) is used to
create a tool definition, and you can supply `parameters` as a
[JSON Schema](https://json-schema.org/) 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.

{/* TODO this all needs to be explained better */}

```tsx
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](https://sonner.emilkowal.ski/), then adds a message in the chat with
[`AiTool`](/docs/api-reference/liveblocks-react-ui#AiTool), letting the user
know that a toast was sent.

```tsx
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.

```tsx
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 [#RegisterAiTool-props]

<PropertiesList>
  <PropertiesListItem name="name" type="string" required>
    Unique name for the tool. This is used internally to identify and manage the
    tool.
  </PropertiesListItem>
  <PropertiesListItem name="tool" type="AiOpaqueToolDefinition" required>
    The tool definition created with
    [`defineAiTool`](/docs/api-reference/liveblocks-client#defineAiTool).
  </PropertiesListItem>
  <PropertiesListItem name="chatId" type="string">
    Optional chat ID to scope this tool to a specific chat. If provided, the
    tool will only be available to that chat.
  </PropertiesListItem>
  <PropertiesListItem name="enabled" type="boolean">
    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`.
  </PropertiesListItem>
</PropertiesList>

## 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](/docs/platform/sync-datastore), initial Presence values for
each user, and Storage values for the room can be set.

```tsx
import { RoomProvider } from "@liveblocks/react/suspense";

function App() {
  return <RoomProvider id="my-room-id">{/* children */}</RoomProvider>;
}
```

<PropertiesList title="Props">
  <PropertiesListItem name="id" type="string" required>
    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](/docs/authentication) for your app, it can helpful to
    decide on a naming pattern for your room IDs.
  </PropertiesListItem>
  <PropertiesListItem name="initialPresence" type="JsonObject">
    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](#setting-initial-presence).
  </PropertiesListItem>
  <PropertiesListItem name="initialStorage" type="LsonObject">
    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](/docs/api-reference/liveblocks-client#Storage). This value is
    ignored after the first render, and if Storage for the current room has
    already been created. [Learn more](#setting-initial-storage).
  </PropertiesListItem>
  <PropertiesListItem name="autoConnect" type="boolean" defaultValue="true">
    Whether the room immediately connects to Liveblocks servers. This value is
    ignored after the first render.
  </PropertiesListItem>
  <PropertiesListItem name="engine" type="1 | 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](/docs/guides/about-the-new-storage-engine).
  </PropertiesListItem>
</PropertiesList>

#### Setting initial Presence [#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`.

```tsx
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 [#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.

```tsx
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](/docs/api-reference/liveblocks-client#Storage)
and JSON-serializable objects are allowed (the `LsonObject` type).

#### Speed up connecting to a room [#speed-up-connecting-to-a-room]

To speed up connecting to a room, you can call
[`Liveblocks.prewarmRoom`](/docs/api-reference/liveblocks-node#get-rooms-roomId-prewarm)
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`.

```ts title="actions.ts"
"use server";

import { Liveblocks } from "@liveblocks/node";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

export async function prewarmRoom(roomId: string) {
  // +++
  await liveblocks.prewarmRoom(roomId);
  // +++
}
```

```tsx title="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

<Banner>

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](#Typing-your-data),
unless you need to define multiple room types in your application.

</Banner>

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.

```tsx file="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 [#createRoomContext-Suspense]

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

```tsx file="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.

```tsx file="liveblocks.config.ts" isCollapsable isCollapsed
import { createClient } from "@liveblocks/client";
import { createRoomContext } from "@liveblocks/react";

const client = createClient({
  // publicApiKey: "",
  // authEndpoint: "/api/liveblocks-auth",
});

// Presence represents the properties that exist on every user in the Room
// and that will automatically be kept in sync. Accessible through the
// `user.presence` property. Must be JSON-serializable.
type Presence = {
  // cursor: { x: number, y: number } | null,
  // ...
};

// Optionally, Storage represents the shared document that persists in the
// Room, even after all users leave. Fields under Storage typically are
// LiveList, LiveMap, LiveObject instances, for which updates are
// automatically persisted and synced to all connected clients.
type Storage = {
  // animals: LiveList<string>,
  // ...
};

// Optionally, UserMeta represents static/readonly metadata on each user, as
// provided by your own custom auth back end (if used). Useful for data that
// will not change during a session, like a user’s name or avatar.
// type UserMeta = {
//   id?: string,  // Accessible through `user.id`
//   info?: Json,  // Accessible through `user.info`
// };

// Optionally, the type of custom events broadcast and listened to in this
// room. Use a union for multiple events. Must be JSON-serializable.
// type RoomEvent = {};

// Optionally, when using Comments, ThreadMetadata represents metadata on
// each thread. Can only contain booleans, strings, and numbers.
// export type ThreadMetadata = {
//   pinned: boolean;
//   quote: string;
//   time: number;
// };

export const {
  RoomProvider,
  useMyPresence,
  useStorage,

  // Other hooks
  // ...
} = createRoomContext<
  Presence,
  Storage
  /* UserMeta, RoomEvent, ThreadMetadata */
>(client);
```

### useRoom [@badge=RoomProvider]

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

```ts
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:

```ts
import { useRoom } from "@liveblocks/react/suspense";

const room = useRoom({ allowOutsideRoom: true }); // Possibly `null`
```

<PropertiesList title="Options">
  <PropertiesListItem
    name="allowOutsideRoom"
    type="boolean"
    defaultValue="false"
  >
    Whether the hook should return `null` instead of throwing when used outside
    of a [`RoomProvider`][] context.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="room" type="Room">
    The Room instance from the nearest [`RoomProvider`][]. Returns `null` if
    `allowOutsideRoom` is `true` and the hook is used outside of a room.
  </PropertiesListItem>
</PropertiesList>

### useIsInsideRoom [@badge=Both]

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

```ts
import { useIsInsideRoom } from "@liveblocks/react/suspense";

const isInsideRoom = useIsInsideRoom();
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem name="isInsideRoom" type="boolean">
    `true` if the hook was called inside a [`RoomProvider`][] context, `false`
    otherwise.
  </PropertiesListItem>
</PropertiesList>

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

```tsx
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`](/docs/api-reference/liveblocks-react#LiveblocksProvider)
and [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider)
contexts.

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


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

// 👤 Just your avatar
<Header />
```

### useStatus [@badge=RoomProvider]

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

```ts
import { useStatus } from "@liveblocks/react/suspense";

const status = useStatus();
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem
    name="status"
    type="'initial' | 'connecting' | 'connected' | 'reconnecting' | 'disconnected'"
  >
    The current WebSocket connection status of the room.
  </PropertiesListItem>
</PropertiesList>

### useSyncStatus [@badge=Both]

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](#display-synchronization-badge).
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```ts
import { useSyncStatus } from "@liveblocks/react/suspense";

// "synchronizing" | "synchronized"
const syncStatus = useSyncStatus();
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="options" type="object">
    Optional configuration object.
  </PropertiesListItem>
  <PropertiesListItem name="options.smooth" type="boolean" defaultValue="false">
    When `true`, prevents quick changes between states by delaying the
    transition from "synchronizing" to "synchronized" until 1 second has passed
    after the final change.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="syncStatus" type="'synchronizing' | 'synchronized'">
    The current synchronization status of Liveblocks.
  </PropertiesListItem>
</PropertiesList>

#### Display a synchronization badge [#display-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.

```tsx
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 [#use-sync-status-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.

```tsx
function Page() {
  return (
    <LiveblocksProvider
      // highlight-next-line
      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](/docs/ready-made-features/text-editor).
- There’s an unsubmitted comment in the
  [Composer](/docs/api-reference/liveblocks-react-ui#Composer).
- The user has made changes and is currently offline.

Internally, this option uses the
[beforeunload event](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event).

### useOthersListener [@badge=RoomProvider]

Calls the given callback when an “others” event occurs, when a user enters,
leaves, or updates their presence.

```ts
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;
    }
  });
}
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="callback" type="(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.

    </PropertiesListItem>

</PropertiesList>

### useLostConnectionListener [@badge=RoomProvider]

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

```ts
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][].

<PropertiesList title="Arguments">
  <PropertiesListItem
    name="callback"
    type="(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".
  </PropertiesListItem>
</PropertiesList>

## Presence

<Banner title="Need help troubleshooting presence?">

Try the [Liveblocks DevTools extension](/devtools) to visualize your
collaborative experiences as you build them, in realtime.

</Banner>

### useMyPresence [@badge=RoomProvider]

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.

```ts
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 }"
```

<PropertiesList title="Returns">
  <PropertiesListItem name="myPresence" type="TPresence">
    The current user’s presence data.
  </PropertiesListItem>
  <PropertiesListItem
    name="updateMyPresence"
    type="(patch: Partial<Presence>, options?: { addToHistory?: boolean }) => void"
  >
    A function to update the current user’s presence. Accepts a partial presence
    object and optional history options.
  </PropertiesListItem>
</PropertiesList>

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

```ts
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.

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

### useUpdateMyPresence [@badge=RoomProvider]

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.

```ts
import { useUpdateMyPresence } from "@liveblocks/react/suspense";

const updateMyPresence = useUpdateMyPresence();

updateMyPresence({ y: 0 });
updateMyPresence({ x: 0 });

// Presence will be { x: 0, y: 0 }
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem
    name="updateMyPresence"
    type="(patch: Partial<Presence>, options?: { addToHistory?: boolean }) => void"
  >
    A function to update the current user's presence. Accepts a partial presence
    object and optional history options.
  </PropertiesListItem>
</PropertiesList>

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

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

### useSelf [@badge=RoomProvider]

Returns the current user once it is connected to the room, and automatically
subscribes to updates to the current user.
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```ts
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][].

<PropertiesList title="Arguments">
  <PropertiesListItem name="selector" type="(me: User) => T">
    Optional selector function to extract specific data from the current user.
    If not provided, returns the entire user object.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="currentUser" type="User | 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).
  </PropertiesListItem>
</PropertiesList>

#### 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](/docs/authentication#id-token-room-permissions).

```ts
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](/docs/api-reference/liveblocks-react-tiptap) as you can prevent
read-only users from editing the document.

```tsx
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>
  );
}
```

### useOthers [@badge=RoomProvider]

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](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

The `others` argument to the `useOthers` selector function is an _immutable_
array of Users.

```tsx
// ✅ Rerenders only if the number of users changes
const numOthers = useOthers((others) => others.length);

// ✅ Rerenders only if someone starts or stops typing
const isSomeoneTyping = useOthers((others) =>
  others.some((other) => other.presence.isTyping)
);

// ✅ Rerenders only if actively typing users are updated
const 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.

```tsx
// ❌ Mapping is hard to get right with this hook
const cursors = useOthers(
  (others) => others.map((other) => other.presence.cursor),
  shallow
);

// ✅ Better to use useOthersMapped
const 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!

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

<Banner type="error" title="Caution">

In production-ready apps, you likely want to avoid calling `useOthers` without
arguments.

</Banner>

<PropertiesList title="Arguments">
  <PropertiesListItem name="selector" type="(others: readonly User[]) => T">
    Optional selector function to extract specific data from the others array.
    If not provided, returns the entire others array.
  </PropertiesListItem>
  <PropertiesListItem name="isEqual" type="(prev: T, curr: T) => boolean">
    Optional equality function to determine if the selected data has changed.
    Defaults to strict equality comparison.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="others" type="readonly 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).
  </PropertiesListItem>
</PropertiesList>

### useOthersMapped [@badge=RoomProvider]

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.

```tsx
// Example 1
const others = useOthersMapped((other) => other.presence.cursor);
// [
//   [2, { x: 27, y: -8 }],
//   [3, { x: 0, y: 19 }],
// ]

// Example 2
const 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.

```tsx
const others = useOthersMapped((other) => other.presence.cursor);

// In JSX
return (
  <>
    {others.map(([connectionId, cursor]) => (
      <Cursor key={connectionId} x={cursor.x} y={cursor.y} />
    ))}
  </>
);
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="selector" type="(other: User) => T">
    A selector function to extract specific data from each user in the others
    array.
  </PropertiesListItem>
  <PropertiesListItem name="isEqual" type="(prev: T, curr: T) => boolean">
    Optional equality function to determine if the selected data for a user has
    changed. Defaults to strict equality comparison.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem
    name="others"
    type="readonly [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).
  </PropertiesListItem>
</PropertiesList>

### useOthersConnectionIds [@badge=RoomProvider]

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](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useOthersConnectionIds } from "@liveblocks/react/suspense";

// [2, 4, 7]
const connectionIds = useOthersConnectionIds();
```

<PropertiesList title="Returns">
  <PropertiesListItem name="connectionIds" type="readonly 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).
  </PropertiesListItem>
</PropertiesList>

#### Another way to fetch connection IDs

This hook is similar to using [`useOthers`][] and calling `.map()` on the
result.

```tsx
import { useOthers, shallow } from "@liveblocks/react/suspense";

// [2, 4, 7]
const connectionIds = useOthers(
  (others) => others.map((other) => other.connectionId),
  shallow
);
```

### useOther [@badge=RoomProvider]

Extract data using a [selector][] for one specific user in the room, and
subscribe to all changes to the selected data.
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
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:

```tsx file="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}
          />
        ))}
      </>
    );
  });
```

```tsx file="Cursor.tsx"
function Cursor({ connectionId }) {
  // +++
  const { x, y } = useOther(connectionId, (other) => other.presence.cursor); // (4)
  // +++
  return <Cursor x={x} y={y} />;
}
```

1. Makes sure this whole component tree will never rerender beyond the first
   time.
2. Makes sure the parent component only rerenders when users join/leave.
3. Makes sure each cursor remains associated to the same connection.
4. Makes sure each cursor rerenders whenever _its_ data changes only.

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

<PropertiesList title="Arguments">
  <PropertiesListItem name="connectionId" type="number">
    The connection ID of the specific user to extract data from.
  </PropertiesListItem>
  <PropertiesListItem name="selector" type="(other: User) => T">
    A selector function to extract specific data from the user.
  </PropertiesListItem>
  <PropertiesListItem name="isEqual" type="(prev: T, curr: T) => boolean">
    Optional equality function to determine if the selected data has changed.
    Defaults to strict equality comparison.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="data" type="T | 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).
  </PropertiesListItem>
</PropertiesList>

## Broadcast

### useBroadcastEvent [@badge=RoomProvider]

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

```ts
import { useBroadcastEvent } from "@liveblocks/react/suspense";

// +++
// On client A
const broadcast = useBroadcastEvent();
broadcast({ type: "EMOJI", emoji: "🔥" });
// +++

// On client B
useEventListener(({ event, user, connectionId }) => {
  //                       ^^^^ Will be Client A
  if (event.type === "EMOJI") {
    // Do something
  }
});
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem name="broadcast" type="(event: TBroadcastEvent) => void">
    A function that broadcasts custom events to other users in the room.
  </PropertiesListItem>
</PropertiesList>

### useEventListener [@badge=RoomProvider]

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](/docs/api-reference/rest-api-endpoints#post-broadcast-event)
REST API, `connectionId` will be `-1`.

```ts
import { useEventListener } from "@liveblocks/react/suspense";

// On client A
const broadcast = useBroadcastEvent();
broadcast({ type: "EMOJI", emoji: "🔥" });

// +++
// On client B
useEventListener(({ 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](https://liveblocks.io/docs/api-reference/rest-api-endpoints#post-broadcast-event).

Automatically unsubscribes when the component is unmounted.

<PropertiesList title="Arguments">
  <PropertiesListItem
    name="callback"
    type="(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.
  </PropertiesListItem>
</PropertiesList>

## 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](/docs/ready-made-features/presence)
which is temporary.

- [`LiveObject`][] - Similar to JavaScript object. Use this for storing records
  with fixed key names and where the values don’t necessarily have the same
  types. For example, a `Person` with a `name: string` and an `age: number`
  field. If multiple clients update the same property simultaneously, the last
  modification received by the Liveblocks servers is the winner.

- [`LiveList`][] - An ordered collection of items synchronized across clients.
  Even if multiple users add/remove/move elements simultaneously, LiveList will
  solve the conflicts to ensure everyone sees the same collection of items.

- [`LiveMap`][] - Similar to a JavaScript Map. Use this for indexing values that
  all have the same structure. For example, to store an index of `Person` values
  by their name. If multiple users update the same property simultaneously, the
  last modification received by the Liveblocks servers is the winner.

### Typing Storage [#typing-storage]

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

```ts
import { LiveList } from "@liveblocks/client";

declare global {
  interface Liveblocks {
    Storage: {
      animals: LiveList<string>;
    };
  }
}
```

You can then set an initial value in [`RoomProvider`][].

```tsx
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](#typing-your-data).

```tsx
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.

```tsx
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.

```ts
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.

```tsx
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>
  );
}
```

<Banner title="Need help troubleshooting Storage?">

Get the [Liveblocks DevTools extension](/devtools) to develop and debug your
application as you build it.

</Banner>

### useStorage [@badge=RoomProvider]

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

```tsx
// ✅ Rerenders if todos (or their children) change
const items = useStorage((root) => root.todos);

// ✅ Rerenders when todos are added or deleted
const numTodos = useStorage((root) => root.todos.length);

// ✅ Rerenders when the value of allDone changes
const allDone = useStorage((root) => root.todos.every((item) => item.done));

// ✅ Rerenders if any _unchecked_ todo items change
const 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][].

<Banner type="success" title="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.

In order to select one item from a LiveMap within the storage tree with the
`useStorage` method, you can use the example below:

```ts
const key = "errands";
const myTodos = useStorage((root) => root.todoMap.get(key));
```

In order to query a LiveMap, and filter for specific values:

```ts
const myTodos = useStorage(
  root => Array.from(root.todoMap.values()).filter(...),
  shallow,
);
```

</Banner>

<PropertiesList title="Arguments">
  <PropertiesListItem name="selector" type="(root: ToJson<TStorage>) => T">
    A selector function to extract specific data from the storage root. The root
    is an immutable copy of your entire Liveblocks Storage tree.
  </PropertiesListItem>
  <PropertiesListItem name="isEqual" type="(prev: T, curr: T) => boolean">
    Optional equality function to determine if the selected data has changed.
    Defaults to strict equality comparison.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="data" type="T | null">
    The selected data from storage. Returns `null` while storage is still
    loading (in non-Suspense version).
  </PropertiesListItem>
</PropertiesList>

### useHistory [@badge=RoomProvider]

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

```ts
import { useHistory } from "@liveblocks/react/suspense";

const { undo, redo, pause, resume, disable } = useHistory();
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem name="history" type="History">
    The room's history object containing methods for
    [`undo`](/docs/api-reference/liveblocks-client#Room.history.undo),
    [`redo`](/docs/api-reference/liveblocks-client#Room.history.redo),
    [`pause`](/docs/api-reference/liveblocks-client#Room.history.pause),
    [`resume`](/docs/api-reference/liveblocks-client#Room.history.resume), and
    [`disable`](/docs/api-reference/liveblocks-client#Room.history.disable)
    operations.
  </PropertiesListItem>
</PropertiesList>

### useUndo [@badge=RoomProvider]

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

```ts
import { useUndo } from "@liveblocks/react/suspense";

const undo = useUndo();
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem name="undo" type="() => void">
    A function that undoes the last operation executed by the current client.
  </PropertiesListItem>
</PropertiesList>

### useRedo [@badge=RoomProvider]

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

```ts
import { useRedo } from "@liveblocks/react/suspense";

const redo = useRedo();
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem name="redo" type="() => void">
    A function that redoes the last operation executed by the current client.
  </PropertiesListItem>
</PropertiesList>

### useCanUndo [@badge=RoomProvider]

Returns whether there are any operations to undo.

```ts
import { useCanUndo, useUpdateMyPresence } from "@liveblocks/react/suspense";

const updateMyPresence = useUpdateMyPresence();
const canUndo = useCanUndo();

updateMyPresence({ y: 0 });

// At the next render, "canUndo" will be true
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem name="canUndo" type="boolean">
    Whether there are any operations to undo.
  </PropertiesListItem>
</PropertiesList>

### useCanRedo [@badge=RoomProvider]

Returns whether there are any operations to redo.

```ts
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
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem name="canRedo" type="boolean">
    Whether there are any operations to redo.
  </PropertiesListItem>
</PropertiesList>

### useMutation [@badge=RoomProvider]

Creates a callback function that lets you mutate Liveblocks state.

```tsx
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" });
  },
  []
);

// JSX
return <button onClick={fillWithRed} />;
```

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

1. Close over a local variable and
   [adding it to the dependency array](#useMutation-dep-arrays), or
2. Have it take [an extra callback parameter](#useMutation-extra-params).

Both are equally fine, just a matter of preference.

<PropertiesList title="Arguments">
  <PropertiesListItem
    name="mutationFn"
    type="(context: MutationContext, ...args: TArgs) => void"
  >
    A function that performs mutations on Liveblocks state. The context provides
    access to `storage`, `setMyPresence`, `self`, and `others`.
  </PropertiesListItem>
  <PropertiesListItem name="deps" type="React.DependencyList">
    A dependency array that determines when the mutation function should be
    recreated, similar to `useCallback`.
  </PropertiesListItem>
</PropertiesList>

#### With dependency arrays [#useMutation-dep-arrays]

```tsx
// Local state maintained outside Liveblocks
const [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
  // +++
);

// JSX
return <button onClick={fillWithCurrentColor} />;
```

<Banner type="success" title="Tip! Let ESLint check your dependencies">

If you use ESLint, we recommend to
[configure it to enforce](/docs/api-reference/liveblocks-react#useMutation-lint-rule)
the correct use of your dependency arrays.

</Banner>

#### With extra callback parameters [#useMutation-extra-params]

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

```tsx
const fill = useMutation(
  // +++
  // Note the second argument
  ({ storage, setMyPresence }, color: string) => {
    // +++
    storage.get("shapes").get("circle1").set("fill", color);
    setMyPresence({ lastUsedColor: color });
  },
  []
);

// JSX
// +++
return <button onClick={() => fill("red")} />;
//                            ^^^^^^^^^^^ Now fill takes a color argument
// +++
```

#### Depending on current presence [#useMutation-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.

```tsx
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: [] });
  },
  []
);

// JSX
return <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 [#useMutation-lint-rule] [@keywords=["exhaustive-deps", "additionalHooks", "eslint-plugin-react-hooks"]]

If you are using ESLint in your project, and are using
[the React hooks plugin](https://reactjs.org/docs/hooks-rules.html#eslint-plugin),
we recommend to add a check for "additional hooks", so that it will also check
the dependency arrays of your `useMutation` calls:

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

<PropertiesList title="Returns">
  <PropertiesListItem name="mutate" type="(...args: TArgs) => void">
    A memoized callback function that executes the mutation. Can accept
    additional arguments that are passed to the mutation function.
  </PropertiesListItem>
</PropertiesList>

## Comments

### useThreads [@badge=RoomProvider]

Returns a paginated list of threads within the current room. Initially fetches
the oldest 50 threads.
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useThreads } from "@liveblocks/react";

const { threads, error, isLoading } = useThreads();
```

Use the [`Thread`](/docs/api-reference/liveblocks-react-ui#Thread) component to
render the oldest 50 threads with our default UI.

```tsx
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>
  );
}
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="options" type="object">
    Optional configuration object.
  </PropertiesListItem>
  <PropertiesListItem name="option.query" type="ThreadsQuery">
    Optional query to filter threads by resolved status and metadata values.
    [Learn more](/docs/api-reference/liveblocks-react#useThreads-query).
  </PropertiesListItem>
  <PropertiesListItem name="option.scrollOnLoad" type="boolean">
    Whether to scroll to a comment if the URL's hash is set to a comment ID.
    Defaults to `true`. [Learn
    more](/docs/api-reference/liveblocks-react#useThreads-scroll).
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="threads" type="ThreadData[] | undefined">
    An array of threads within the current room, or `undefined` if not yet
    loaded (in non-Suspense version).
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the threads are currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the threads.
  </PropertiesListItem>
  <PropertiesListItem name="hasFetchedAll" type="boolean">
    Whether all available threads have been fetched. [Learn
    more](/docs/api-reference/liveblocks-react#useThreads-pagination).
  </PropertiesListItem>
  <PropertiesListItem name="fetchMore" type="() => void">
    A function to fetch more threads. [Learn
    more](/docs/api-reference/liveblocks-react#useThreads-pagination).
  </PropertiesListItem>
  <PropertiesListItem name="isFetchingMore" type="boolean">
    Whether more threads are currently being fetched. [Learn
    more](/docs/api-reference/liveblocks-react#useThreads-pagination).
  </PropertiesListItem>
  <PropertiesListItem name="fetchMoreError" type="Error | undefined">
    Any error that occurred while fetching more threads. [Learn
    more](/docs/api-reference/liveblocks-react#useThreads-pagination).
  </PropertiesListItem>
</PropertiesList>

#### Querying threads [#useThreads-query]

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.

```tsx
// 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 [#useThreads-pagination]

By default, the `useThreads` hook returns up to 50 threads. To fetch more, the
hook provides additional fields for pagination, similar to
[`useInboxNotifications`][].

```tsx
import { useThreads } from "@liveblocks/react";

const {
  isLoading,
  error,
  threads,

  +++
  fetchMore,
  isFetchingMore,
  hasFetchedAll,
  fetchMoreError,
  +++
} = useThreads();
```

- `hasFetchedAll` indicates whether all available threads have been fetched.
- `fetchMore` loads up to 50 more threads, and is always safe to call.
- `isFetchingMore` indicates whether more threads are being fetched.
- `fetchMoreError` returns error statuses resulting from fetching more.

##### Pagination example [#useThreads-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.

```tsx
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 [#useThreads-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.

```tsx
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 [#useThreads-scroll]

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

```tsx
const { threads } = useThreads({ scrollOnLoad: false });
```

### useCreateThread [@badge=RoomProvider]

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

```tsx
import { useCreateThread } from "@liveblocks/react/suspense";

const createThread = useCreateThread();
const thread = createThread({
  body: {},
  attachments: [],
  metadata: {},
  commentMetadata: {},
});
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="createThread"
    type="(options: CreateThreadOptions) => ThreadData"
  >
    A function that creates a thread with an initial comment, and optionally
    thread and comment metadata. Returns the optimistic thread object.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useCreateThread-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.

```tsx
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}`);
  }
});
```

### useSearchComments [@badge=RoomProvider]

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](#Create-a-comments-search-bar).

```tsx
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.

<PropertiesList title="Arguments">
  <PropertiesListItem name="options" type="object">
    Configuration object.
  </PropertiesListItem>
  <PropertiesListItem name="options.query" type="SearchCommentsQuery">
    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.
  </PropertiesListItem>
  <PropertiesListItem name="options.query.text" type="string" required>
    Text to search within comment content. Uses rich text and vector search for
    relevance.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.query.threadMetadata"
    type="Partial<QueryMetadata>"
  >
    Metadata to filter threads by.
  </PropertiesListItem>
  <PropertiesListItem name="options.query.threadResolved" type="boolean">
    Whether to only return comments from threads that are resolved.
  </PropertiesListItem>
  <PropertiesListItem name="options.query.hasAttachments" type="boolean">
    Whether to only return comments that have attachments.
  </PropertiesListItem>
  <PropertiesListItem name="options.query.hasMentions" type="boolean">
    Whether to only return comments that have mentions.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="results" type="ThreadData[] | undefined">
    An array of matched comments with the parent thread they belong to.
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the results are currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while searching the threads.
  </PropertiesListItem>
</PropertiesList>

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

```tsx
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”.

```tsx title="Resolved/unresolved filter" isCollapsable isCollapsed
import { useSearchComments } from "@liveblocks/react";
import { useState } from "react";

type ThreadResolved = "all" | "resolved" | "unresolved";

function SearchBar() {
  const [search, setSearch] = useState("");
  // +++
  const [searchResolved, searchResolved] = useState<ThreadResolved>("all");
  // +++

  // +++
  const searchThreadResolved =
    searchResolved === "all" ? undefined : searchResolved === "resolved";
  // +++

  const { results, isLoading, error } = useSearchComments({
    query: {
      text: search,
      // +++
      threadResolved: searchThreadResolved,
      // +++
    },
  });

  return (
    <>
      <input
        type="text"
        value={search}
        onChange={(e) => setSearch(e.target.value)}
      />
      // +++
      <button
        data-active={searchResolved === "all"}
        onClick={() => setSearchResolved("all")}
      >
        All
      </button>
      <button
        data-active={searchResolved === "resolved"}
        onClick={() => setSearchResolved("resolved")}
      >
        Resolved
      </button>
      <button
        data-active={searchResolved === "unresolved"}
        onClick={() => setSearchResolved("unresolved")}
      >
        Resolved
      </button>
      // +++
      {isLoading ? (
        <div>Loading...</div>
      ) : (
        results.map((result) => (
          <a key={result.commentId} href={"#" + result.commentId}>
            {result.content}
          </a>
        ))
      )}
    </>
  );
}
```

### useDeleteThread [@badge=RoomProvider]

Returns a function that deletes a thread and all its associated comments by ID.
Only the thread creator can delete the thread.

```tsx
import { useDeleteThread } from "@liveblocks/react/suspense";

const deleteThread = useDeleteThread();
deleteThread("th_xxx");
```

<PropertiesList title="Returns">
  <PropertiesListItem name="deleteThread" type="(threadId: string) => void">
    A function that deletes a thread and all its associated comments by ID.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useDeleteThread-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.

```tsx
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}`);
  }
});
```

### useEditThreadMetadata [@badge=RoomProvider]

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.

```tsx
import { useEditThreadMetadata } from "@liveblocks/react/suspense";

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

<PropertiesList title="Returns">
  <PropertiesListItem
    name="editThreadMetadata"
    type="(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`.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useEditThreadMetadata-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.

```tsx
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}`);
  }
});
```

### useMarkThreadAsResolved [@badge=RoomProvider]

Returns a function that marks a thread as resolved.

```tsx
import { useMarkThreadAsResolved } from "@liveblocks/react/suspense";

const markThreadAsResolved = useMarkThreadAsResolved();
markThreadAsResolved("th_xxx");
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="markThreadAsResolved"
    type="(threadId: string) => void"
  >
    A function that marks a thread as resolved.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useMarkThreadAsResolved-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.

```tsx
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}`);
  }
});
```

### useMarkThreadAsUnresolved [@badge=RoomProvider]

Returns a function that marks a thread as unresolved.

```tsx
import { useMarkThreadAsUnresolved } from "@liveblocks/react/suspense";

const markThreadAsUnresolved = useMarkThreadAsUnresolved();
markThreadAsUnresolved("th_xxx");
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="markThreadAsUnresolved"
    type="(threadId: string) => void"
  >
    A function that marks a thread as unresolved.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useMarkThreadAsUnresolved-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.

```tsx
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}`);
  }
});
```

### useMarkThreadAsRead [@badge=RoomProvider]

Returns a function that marks a thread as read.

```tsx
import { useMarkThreadAsRead } from "@liveblocks/react/suspense";

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

<PropertiesList title="Returns">
  <PropertiesListItem name="markThreadAsRead" type="(threadId: string) => void">
    A function that marks a thread as read.
  </PropertiesListItem>
</PropertiesList>

### useThreadSubscription [@badge=RoomProvider]

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.

```tsx
import { useThreadSubscription } from "@liveblocks/react/suspense";

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

`subscribe` and `unsubscribe` work similarly to
[`useSubscribeToThread`](#useSubscribeToThread) and
[`useUnsubscribeFromThread`](#useUnsubscribeFromThread), but they only affect
the current thread.

<PropertiesList title="Arguments">
  <PropertiesListItem name="threadId" type="string">
    The ID of the thread to get subscription status for.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="status" type="ThreadSubscriptionStatus">
    The subscription status of the thread ('subscribed', 'unsubscribed', or
    'not_subscribed').
  </PropertiesListItem>
  <PropertiesListItem name="subscribe" type="() => void">
    A function to subscribe to the thread.
  </PropertiesListItem>
  <PropertiesListItem name="unsubscribe" type="() => void">
    A function to unsubscribe from the thread.
  </PropertiesListItem>
  <PropertiesListItem name="unreadSince" type="Date | null">
    The date when the thread was last read, or null if it has been read.
  </PropertiesListItem>
</PropertiesList>

### useSubscribeToThread [@badge=RoomProvider]

Returns a function that subscribes the current user to a thread, meaning they
will receive inbox notifications when new comments are posted.

```tsx
import { useSubscribeToThread } from "@liveblocks/react/suspense";

const subscribeToThread = useSubscribeToThread();
subscribeToThread("th_xxx");
```

#### Error handling [#useSubscribeToThread-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.

```tsx
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](#useRoomSubscriptionSettings). This value can also be
overridden by a room-level call that is run afterwards.

```ts
const subscribeToThread = useSubscribeToThread();
const [{ settings }, updateSettings] = useRoomSubscriptionSettings();

// 1. Disables notifications for all threads
updateSettings({
  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",
});
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="subscribeToThread"
    type="(threadId: string) => void"
  >
    A function that subscribes the current user to a thread for inbox
    notifications.
  </PropertiesListItem>
</PropertiesList>

### useUnsubscribeFromThread [@badge=RoomProvider]

Returns a function that unsubscribes the current user from a thread, meaning
they will no longer receive inbox notifications when new comments are posted.

```tsx
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](#useRoomSubscriptionSettings). This value can also be
overridden by a room-level call that is run afterwards.

```ts
const subscribeToThread = useSubscribeToThread();
const [{ settings }, updateSettings] = useRoomSubscriptionSettings();

// 1. Enables notifications for all threads
updateSettings({
  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",
});
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="unsubscribeFromThread"
    type="(threadId: string) => void"
  >
    A function that unsubscribes the current user from a thread, stopping inbox
    notifications.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useUnsubscribeFromThread-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.

```tsx
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}`);
  }
});
```

### useCreateComment [@badge=RoomProvider]

Returns a function that adds a comment to a thread.

```tsx
import { useCreateComment } from "@liveblocks/react/suspense";

const createComment = useCreateComment();
const comment = createComment({
  threadId: "th_xxx",
  body: {},
  attachments: [],
  metadata: {},
});
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="createComment"
    type="(options: CreateCommentOptions) => CommentData"
  >
    A function that adds a comment to a thread. Returns the optimistic comment
    object.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useCreateComment-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.

```tsx
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}`);
  }
});
```

### useEditComment[@badge=RoomProvider]

Returns a function that edits a comment’s body, and optionally its attachments
and metadata.

```tsx
import { useEditComment } from "@liveblocks/react/suspense";

const editComment = useEditComment();
editComment({
  threadId: "th_xxx",
  commentId: "cm_xxx",
  body: {},
  attachments: [],
  metadata: {},
});
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="editComment"
    type="(options: EditCommentOptions) => void"
  >
    A function that edits a comment’s body, and optionally its attachments and
    metadata.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useEditComment-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.

```tsx
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}`);
  }
});
```

### useEditCommentMetadata [@badge=RoomProvider]

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.

```tsx
import { useEditCommentMetadata } from "@liveblocks/react/suspense";

const editCommentMetadata = useEditCommentMetadata();
editCommentMetadata({
  threadId: "th_xxx",
  commentId: "cm_xxx",
  metadata: {},
});
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="editCommentMetadata"
    type="(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`.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useEditCommentMetadata-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.

```tsx
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}`);
  }
});
```

### useDeleteComment [@badge=RoomProvider]

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

```tsx
import { useDeleteComment } from "@liveblocks/react/suspense";

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

<PropertiesList title="Returns">
  <PropertiesListItem
    name="deleteComment"
    type="(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.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useDeleteComment-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.

```tsx
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}`);
  }
});
```

### useAddReaction [@badge=RoomProvider]

Returns a function that adds a reaction to a comment. Can be used to create an
[emoji picker](/docs/api-reference/liveblocks-react-ui#emoji-picker) or
[emoji reactions](/docs/api-reference/liveblocks-react-ui#emoji-reactions).

```tsx
import { useAddReaction } from "@liveblocks/react/suspense";

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

<PropertiesList title="Returns">
  <PropertiesListItem
    name="addReaction"
    type="(options: { threadId: string; commentId: string; emoji: string }) => void"
  >
    A function that adds a reaction to a comment.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useAddReaction-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.

```tsx
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}`);
  }
});
```

### useRemoveReaction [@badge=RoomProvider]

Returns a function that removes a reaction from a comment. Can be used to create
an [emoji picker](/docs/api-reference/liveblocks-react-ui#emoji-picker) or
[emoji reactions](/docs/api-reference/liveblocks-react-ui#emoji-reactions)

```tsx
import { useRemoveReaction } from "@liveblocks/react/suspense";

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

<PropertiesList title="Returns">
  <PropertiesListItem
    name="removeReaction"
    type="(options: { threadId: string; commentId: string; emoji: string }) => void"
  >
    A function that removes a reaction from a comment.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useRemoveReaction-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.

```tsx
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}`);
  }
});
```

### useAttachmentUrl [@badge=RoomProvider]

Returns a presigned URL for an attachment by its ID.
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useAttachmentUrl } from "@liveblocks/react";

const { url, error, isLoading } = useAttachmentUrl("at_xxx");
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="attachmentId" type="string">
    The ID of the attachment to get a presigned URL for.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="url" type="string | undefined">
    The presigned URL for the attachment, or `undefined` if not found or not yet
    loaded (in non-Suspense version).
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the URL is currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the URL.
  </PropertiesListItem>
</PropertiesList>

## Feeds

### useFeeds [@badge=RoomProvider]

Returns a paginated list of feeds within the current room. Results are sorted
oldest first and can be [filtered](#useFeeds-filtering) and
[paginated](#useFeeds-pagination).
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useFeeds } from "@liveblocks/react";

const { feeds, error, isLoading } = useFeeds();
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="options" type="object">
    Optional configuration object.
  </PropertiesListItem>
  <PropertiesListItem name="options.since" type="number">
    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).
  </PropertiesListItem>
  <PropertiesListItem
    name="options.metadata"
    type="Record&lt;string, string&gt;"
  >
    Optional metadata filter. Only feeds whose metadata matches every key/value
    pair are included (applied to the cached data for this hook’s options).
  </PropertiesListItem>
  <PropertiesListItem name="options.limit" type="number">
    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.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="feeds" type="Feed[] | 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).
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the feeds are currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the feeds.
  </PropertiesListItem>
  <PropertiesListItem name="hasFetchedAll" type="boolean">
    Whether all available feeds have been fetched.
  </PropertiesListItem>
  <PropertiesListItem name="fetchMore" type="() => void">
    A function to fetch more feeds.
  </PropertiesListItem>
  <PropertiesListItem name="isFetchingMore" type="boolean">
    Whether more feeds are currently being fetched.
  </PropertiesListItem>
  <PropertiesListItem name="fetchMoreError" type="Error | undefined">
    Any error that occurred while fetching more feeds.
  </PropertiesListItem>
</PropertiesList>

#### Filtering [#useFeeds-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]().

```tsx
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 [#useFeeds-pagination]

By default, the `useFeeds` hook returns up to 50 feeds. To fetch more, the hook
provides additional fields for pagination, similar to [`useThreads`][].

```tsx
import { useFeeds } from "@liveblocks/react";

const {
  feeds,
  error,
  isLoading,

  // +++
  fetchMore,
  isFetchingMore,
  hasFetchedAll,
  fetchMoreError,
  // +++
} = useFeeds();
```

- `hasFetchedAll` indicates whether all available feeds have been fetched.
- `fetchMore` loads up to 50 more feeds, and is always safe to call.
- `isFetchingMore` indicates whether more feeds are being fetched.
- `fetchMoreError` returns error statuses resulting from fetching more.

##### Pagination example [#useFeeds-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.

```tsx
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>
  );
}
```

### useFeedMessages [@badge=RoomProvider]

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](#useFeedMessages-pagination).
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useFeedMessages } from "@liveblocks/react";

const { messages, error, isLoading } = useFeedMessages("my-feed-id");
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="feedId" type="string" required>
    The ID of the feed to get messages from.
  </PropertiesListItem>
  <PropertiesListItem name="options" type="object">
    Optional configuration object.
  </PropertiesListItem>
  <PropertiesListItem name="options.cursor" type="string">
    Optional cursor for pagination.
  </PropertiesListItem>
  <PropertiesListItem name="options.limit" type="number">
    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.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="messages" type="FeedMessage[] | undefined">
    An array of messages in the feed (chronological order by `createdAt`), or
    `undefined` if not yet loaded (in non-Suspense version).
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the messages are currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the messages.
  </PropertiesListItem>
  <PropertiesListItem name="hasFetchedAll" type="boolean">
    Whether all available messages have been fetched.
  </PropertiesListItem>
  <PropertiesListItem name="fetchMore" type="() => void">
    A function to fetch more messages.
  </PropertiesListItem>
  <PropertiesListItem name="isFetchingMore" type="boolean">
    Whether more messages are currently being fetched.
  </PropertiesListItem>
  <PropertiesListItem name="fetchMoreError" type="Error | undefined">
    Any error that occurred while fetching more messages.
  </PropertiesListItem>
</PropertiesList>

#### Pagination [#useFeedMessages-pagination]

By default, the `useFeedMessages` hook returns up to 50 messages. To fetch more,
the hook provides additional fields for pagination, similar to [`useThreads`][].

```tsx
import { useFeedMessages } from "@liveblocks/react";

const {
  messages,
  error,
  isLoading,

  // +++
  hasFetchedAll,
  fetchMore,
  isFetchingMore,
  fetchMoreError,
  // +++
} = useFeedMessages("my-feed-id");
```

- `hasFetchedAll` indicates whether all available messages have been fetched.
- `fetchMore` loads up to 50 more messages, and is always safe to call.
- `isFetchingMore` indicates whether more messages are being fetched.
- `fetchMoreError` returns error statuses resulting from fetching more.

##### Pagination example [#useFeedMessages-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.

```tsx
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>
  );
}
```

### useCreateFeed [@badge=RoomProvider]

Returns a function that creates a new feed in the current room.

```tsx
import { useCreateFeed } from "@liveblocks/react";

const createFeed = useCreateFeed();
createFeed("my-feed-id", {
  metadata: { name: "My Feed", channel: true },
  timestamp: Date.now(),
});
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="createFeed"
    type="(feedId: string, options?: { metadata?: JsonObject; timestamp?: number }) => void"
  >
    A function that creates a feed. Takes the feed ID and optional metadata and
    timestamp.
  </PropertiesListItem>
</PropertiesList>

### useDeleteFeed [@badge=RoomProvider]

Returns a function that deletes a feed from the current room.

```tsx
import { useDeleteFeed } from "@liveblocks/react";

const deleteFeed = useDeleteFeed();
deleteFeed("my-feed-id");
```

<PropertiesList title="Returns">
  <PropertiesListItem name="deleteFeed" type="(feedId: string) => void">
    A function that deletes a feed. Takes the feed ID.
  </PropertiesListItem>
</PropertiesList>

### useUpdateFeedMetadata [@badge=RoomProvider]

Returns a function that updates a feed's metadata in the current room.

```tsx
import { useUpdateFeedMetadata } from "@liveblocks/react";

const updateFeedMetadata = useUpdateFeedMetadata();
updateFeedMetadata("my-feed-id", {
  name: "Updated Name",
  updated: new Date().toISOString(),
});
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="updateFeedMetadata"
    type="(feedId: string, metadata: JsonObject) => void"
  >
    A function that updates a feed's metadata. Takes the feed ID and the new
    metadata object.
  </PropertiesListItem>
</PropertiesList>

### useCreateFeedMessage [@badge=RoomProvider]

Returns a function that adds a new message to a feed in the current room.

```tsx
import { useCreateFeedMessage } from "@liveblocks/react";

const createFeedMessage = useCreateFeedMessage();
createFeedMessage("my-feed-id", { role: "user", content: "Hello, world!" });

// With optional id and timestamp
createFeedMessage(
  "my-feed-id",
  { role: "user", content: "Hello!" },
  {
    id: "my-message-id",
    timestamp: Date.now(),
  }
);
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="createFeedMessage"
    type="(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.
  </PropertiesListItem>
</PropertiesList>

### useDeleteFeedMessage [@badge=RoomProvider]

Returns a function that deletes a message from a feed in the current room.

```tsx
import { useDeleteFeedMessage } from "@liveblocks/react";

const deleteFeedMessage = useDeleteFeedMessage();
deleteFeedMessage("my-feed-id", "my-message-id");
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="deleteFeedMessage"
    type="(feedId: string, messageId: string) => void"
  >
    A function that deletes a feed message. Takes the feed ID and message ID.
  </PropertiesListItem>
</PropertiesList>

### useUpdateFeedMessage [@badge=RoomProvider]

Returns a function that updates a feed message in the current room.

```tsx
import { useUpdateFeedMessage } from "@liveblocks/react";

const updateFeedMessage = useUpdateFeedMessage();
updateFeedMessage("my-feed-id", "my-message-id", {
  role: "user",
  content: "Updated content",
});
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="updateFeedMessage"
    type="(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.
  </PropertiesListItem>
</PropertiesList>

#### Typing feed metadata and message data [#feeds-typescript]

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`](#useFeeds) and [`useFeedMessages`](#useFeedMessages).

```ts
// liveblocks.config.ts
declare global {
  interface Liveblocks {
    FeedMetadata: {
      name?: string;
      channel?: boolean;
      agentName?: string;
    };

    FeedMessageData: {
      role: "user" | "assistant" | "system";
      content: string;
    };
  }
}
```

## Notifications

### useInboxNotifications [@badge=LiveblocksProvider]

Returns a paginated list of inbox notifications for the current user. Initially
fetches the latest 50 items. Inbox notifications are
[project-based](/docs/ready-made-features/notifications/concepts#Project-based),
meaning notifications from outside the current room are received.

```tsx
import { useInboxNotifications } from "@liveblocks/react";

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

Use the
[`InboxNotification`](/docs/api-reference/liveblocks-react-ui#InboxNotification)
component to render the latest 50 inbox notifications with our default UI.

```tsx
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>
  );
}
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="options" type="object">
    Optional configuration object.
  </PropertiesListItem>
  <PropertiesListItem name="options.query" type="InboxNotificationsQuery">
    Optional query to filter notifications by room ID or kind. [Learn
    more](/docs/api-reference/liveblocks-react#useInboxNotifications-query).
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem
    name="inboxNotifications"
    type="InboxNotificationData[] | undefined"
  >
    An array of inbox notifications for the current user, or `undefined` if not
    yet loaded (in non-Suspense version).
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the notifications are currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the notifications.
  </PropertiesListItem>
  <PropertiesListItem name="hasFetchedAll" type="boolean">
    Whether all available notifications have been fetched. [Learn
    more](/docs/api-reference/liveblocks-react#useInboxNotifications-pagination).
  </PropertiesListItem>
  <PropertiesListItem name="fetchMore" type="() => void">
    A function to fetch more notifications. [Learn
    more](/docs/api-reference/liveblocks-react#useInboxNotifications-pagination).
  </PropertiesListItem>
  <PropertiesListItem name="isFetchingMore" type="boolean">
    Whether more notifications are currently being fetched. [Learn
    more](/docs/api-reference/liveblocks-react#useInboxNotifications-pagination).
  </PropertiesListItem>
  <PropertiesListItem name="fetchMoreError" type="Error | undefined">
    Any error that occurred while fetching more notifications. [Learn
    more](/docs/api-reference/liveblocks-react#useInboxNotifications-pagination).
  </PropertiesListItem>
</PropertiesList>

#### Querying inbox notifications [#useInboxNotifications-query]

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.

```tsx
// 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 [#useInboxNotifications-pagination]

By default, the `useInboxNotifications` hook returns up to 50 notifications. To
fetch more, the hook provides additional fields for pagination, similar to
[`useThreads`][].

```tsx
import { useInboxNotifications } from "@liveblocks/react";

const {
  inboxNotifications,
  isLoading,
  error,

  +++
  hasFetchedAll,
  fetchMore,
  isFetchingMore,
  fetchMoreError,
  +++
} = useInboxNotifications();
```

- `hasFetchedAll` indicates whether all available inbox notifications have been
  fetched.
- `fetchMore` loads up to 50 more notifications, and is always safe to call.
- `isFetchingMore` indicates whether more notifications are being fetched.
- `fetchMoreError` returns error statuses resulting from fetching more.

##### Pagination example [#useInboxNotifications-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.

```tsx
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 [#useInboxNotifications-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.

```tsx
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 [#useInboxNotifications-batched-notifications]

If you’re
[batching custom notifications](/docs/api-reference/liveblocks-node#Batching-custom-notifications),
you can render each activity inside a single notification. The `activities`
array will contain multiple items.

```tsx
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);

  // ...
}
```

### useUnreadInboxNotificationsCount [@badge=LiveblocksProvider]

Returns the number of unread inbox notifications for the current user.
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useUnreadInboxNotificationsCount } from "@liveblocks/react";

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

<PropertiesList title="Arguments">
  <PropertiesListItem
    name="options"
    type="UseUnreadInboxNotificationsCountOptions"
  >
    Optional configuration object.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Options">
  <PropertiesListItem name="query" type="InboxNotificationsQuery">
    Optional query to filter notifications count by room ID or kind.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="count" type="number | undefined">
    The number of unread inbox notifications for the current user, or
    `undefined` if not yet loaded (in non-Suspense version).
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the number of unread inbox notifications is currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the number of unread inbox
    notifications.
  </PropertiesListItem>
</PropertiesList>

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

```tsx
// 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",
  },
});
```

### useMarkInboxNotificationAsRead [@badge=LiveblocksProvider]

Returns a function that marks an inbox notification as read for the current
user.

```tsx
import { useMarkInboxNotificationAsRead } from "@liveblocks/react/suspense";

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

<PropertiesList title="Returns">
  <PropertiesListItem
    name="markInboxNotificationAsRead"
    type="(inboxNotificationId: string) => void"
  >
    A function that marks an inbox notification as read for the current user.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useMarkInboxNotificationAsRead-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.

```tsx
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}`);
  }
});
```

### useMarkAllInboxNotificationsAsRead [@badge=LiveblocksProvider]

Returns a function that marks all of the current user‘s inbox notifications as
read.

```tsx
import { useMarkAllInboxNotificationsAsRead } from "@liveblocks/react/suspense";

const markAllInboxNotificationsAsRead = useMarkAllInboxNotificationsAsRead();
markAllInboxNotificationsAsRead();
```

<PropertiesList title="Returns">
  <PropertiesListItem name="markAllInboxNotificationsAsRead" type="() => void">
    A function that marks all of the current user's inbox notifications as read.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useMarkAllInboxNotificationsAsRead-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.

```tsx
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");
  }
});
```

### useDeleteInboxNotification [@badge=LiveblocksProvider]

Returns a function that deletes an inbox notification for the current user.

```tsx
import { useDeleteInboxNotification } from "@liveblocks/react/suspense";

const deleteInboxNotification = useDeleteInboxNotification();
deleteInboxNotification("in_xxx");
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="deleteInboxNotification"
    type="(inboxNotificationId: string) => void"
  >
    A function that deletes an inbox notification for the current user.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useDeleteInboxNotification-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.

```tsx
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}`);
  }
});
```

### useDeleteAllInboxNotifications [@badge=LiveblocksProvider]

Returns a function that deletes all of the current user‘s inbox notifications.

```tsx
import { useDeleteAllInboxNotifications } from "@liveblocks/react/suspense";

const deleteAllInboxNotifications = useDeleteAllInboxNotifications();
deleteAllInboxNotifications();
```

<PropertiesList title="Returns">
  <PropertiesListItem name="deleteAllInboxNotifications" type="() => void">
    A function that deletes all of the current user’s inbox notifications.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useDeleteAllInboxNotifications-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.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  if (error.context.type === "DELETE_ALL_INBOX_NOTIFICATIONS_ERROR") {
    console.log("Problem deleting all notifications");
  }
});
```

### useInboxNotificationThread [@badge=LiveblocksProvider]

Returns the thread associated with a `"thread"` inbox notification.

```tsx
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](/docs/api-reference/liveblocks-react-ui#Rendering-notification-kinds-differently)
or in other situations where you can guarantee the kind of the notification.

<Banner type="info" title="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.

</Banner>

<PropertiesList title="Arguments">
  <PropertiesListItem name="inboxNotificationId" type="string">
    The ID of the inbox notification to get the associated thread for. Must be a
    "thread" type notification.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="thread" type="ThreadData | null">
    The thread associated with the inbox notification, or null if not found.
  </PropertiesListItem>
</PropertiesList>

### useRoomSubscriptionSettings [@badge=RoomProvider]

Returns the user’s subscription settings for the current room and a function to
update them. Updating this setting will change which
[`inboxNotifications`](#useInboxNotifications) the current user receives in the
current room.

```tsx
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 room
updateSettings({
  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.

<PropertiesList title="Returns">
  <PropertiesListItem name="settings" type="RoomSubscriptionSettings">
    The current subscription settings for the room.
  </PropertiesListItem>
  <PropertiesListItem
    name="updateSettings"
    type="(settings: Partial<RoomSubscriptionSettings>) => void"
  >
    A function to update the subscription settings.
  </PropertiesListItem>
</PropertiesList>

### useUpdateRoomSubscriptionSettings [@badge=RoomProvider]

Returns a function that updates the user’s notification settings for the current
room. Updating this setting will change which
[`inboxNotifications`](#useInboxNotifications) the current user receives in the
current room.

```tsx
import { useUpdateRoomSubscriptionSettings } from "@liveblocks/react/suspense";

const updateRoomSubscriptionSettings = useUpdateRoomSubscriptionSettings();

// No longer receive thread notifications in this room
updateSettings({
  threads: "none",
});
```

#### Error handling [#useUpdateRoomSubscriptionSettings-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.

```tsx
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`](#useRoomSubscriptionSettings).

<PropertiesList title="Returns">
  <PropertiesListItem
    name="updateRoomSubscriptionSettings"
    type="(settings: Partial<RoomSubscriptionSettings>) => void"
  >
    A function that updates the user's notification settings for the current
    room.
  </PropertiesListItem>
</PropertiesList>

##### Replacing individual thread subscriptions

Subscribing will replace any
[existing thread subscriptions](#useSubscribeToThread) in the current room. This
value can also be overridden by a room-level call that is run afterwards.

```ts
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 [#useUpdateRoomSubscriptionSettings-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.

```tsx
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}`);
  }
});
```

### useNotificationSettings [@badge=LiveblocksProvider]

<Banner>

Notification settings is currently in beta.

</Banner>

Returns the user’s notification settings in the current project, in other words
which [notification webhook events](/docs/platform/webhooks#NotificationEvent)
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](/docs/guides/how-to-create-a-notification-settings-panel).

```tsx
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 channel
updateSettings({
  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:

```ts
const [{ isLoading, error, settings }, updateSettings] =
  useNotificationSettings();

// { email: null, ... }
console.log(settings);
```

#### Updating notification settings [#useNotificationSettings-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](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
// You only need to pass partials
updateSettings({
  email: { thread: true },
});

// Enabling a custom notification on the slack channel
updateSettings({
  slack: { $myCustomNotification: true },
});

// Setting complex settings
updateSettings({
  email: {
    thread: true,
    textMention: false,
    $newDocument: true,
  },
  slack: {
    thread: false,
    $fileUpload: false,
  },
  teams: {
    thread: true,
  },
});
```

Subscribing will replace any
[existing thread subscriptions](#useSubscribeToThread) in the current room. This
value can also be overridden by a room-level call that is run afterwards.

```ts
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",
});
```

<PropertiesList title="Returns">
  <PropertiesListItem name="settings" type="NotificationSettings | null">
    The current notification settings for the user, or null if no settings are
    configured.
  </PropertiesListItem>
  <PropertiesListItem
    name="updateSettings"
    type="(settings: Partial<NotificationSettings>) => void"
  >
    A function to update the notification settings.
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the notification settings are currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | null">
    Any error that occurred while loading the notification settings.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useNotificationSettings-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.

```tsx
import { useNotificationSettings } from "@liveblocks/react";

const [{ isLoading, error, settings }, updateSettings] =
  useNotificationSettings();

if (error) {
  return (
    <div>
      <p>Error loading notification settings: {error.message}</p>
    </div>
  );
}
```

### useUpdateNotificationSettings [@badge=LiveblocksProvider]

<Banner>

Notification settings is currently in beta.

</Banner>

Returns a function that updates user’s notification settings, which affects
which [notification webhook events](/docs/platform/webhooks#NotificationEvent)
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](/docs/guides/how-to-create-a-notification-settings-panel).

```tsx
import { useUpdateNotificationSettings } from "@liveblocks/react";

const updateSettings = useUpdateNotificationSettings();

// Disabling thread notifications on the email channel
updateSettings({
  email: { thread: false },
});
```

Works the same as `updateSettings` in
[`useNotificationSettings`](#useNotificationSettings). You can pass a partial
object, or many settings at once.

```tsx
// You only need to pass partials
updateSettings({
  email: { thread: true },
});

// Enabling a custom notification on the slack channel
updateSettings({
  slack: { $myCustomNotification: true },
});

// Setting complex settings
updateSettings({
  email: {
    thread: true,
    textMention: false,
    $newDocument: true,
  },
  slack: {
    thread: false,
    $fileUpload: false,
  },
  teams: {
    thread: true,
  },
});
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="updateNotificationSettings"
    type="(settings: Partial<NotificationSettings>) => void"
  >
    A function that updates the user's notification settings for the current
    project.
  </PropertiesListItem>
</PropertiesList>

## Version History

### useHistoryVersions [@badge=RoomProvider]

Returns the versions of the room. See
[Version History Components](/docs/api-reference/liveblocks-react-ui#Version-history-components)
for more information on how to display versions.

```tsx
import { useHistoryVersions } from "@liveblocks/react";

const { versions, error, isLoading } = useHistoryVersions();
```

<PropertiesList title="Returns">
  <PropertiesListItem name="versions" type="HistoryVersion[]">
    An array of history versions for the room.
  </PropertiesListItem>
</PropertiesList>

## Miscellaneous

### useUser [@badge=Both]

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](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useUser } from "@liveblocks/react";

const { user, error, isLoading } = useUser("user-id");
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="userId" type="string">
    The ID of the user to get information for.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="user" type="User | undefined">
    The user information, or `undefined` if not found or not yet loaded (in
    non-Suspense version).
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the user information is currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the user information.
  </PropertiesListItem>
</PropertiesList>

### useRoomInfo [@badge=Both]

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](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useRoomInfo } from "@liveblocks/react";

const { info, error, isLoading } = useRoomInfo("room-id");
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="roomId" type="string">
    The ID of the room to get information for.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="info" type="RoomInfo | undefined">
    The room information, or `undefined` if not found or not yet loaded (in
    non-Suspense version).
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the room information is currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the room information.
  </PropertiesListItem>
</PropertiesList>

### useGroupInfo [@badge=Both]

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](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useGroupInfo } from "@liveblocks/react";

const { info, error, isLoading } = useGroupInfo("group-id");
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="groupId" type="string">
    The ID of the group to get information for.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="info" type="GroupInfo | undefined">
    The group information, or `undefined` if not found or not yet loaded (in
    non-Suspense version).
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the group information is currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the group information.
  </PropertiesListItem>
</PropertiesList>

### useUrlMetadata [@badge=Both]

Returns metadata for a given URL.
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useUrlMetadata } from "@liveblocks/react";

const { metadata, error, isLoading } = useUrlMetadata("https://liveblocks.io");

// metadata.title, metadata.description, metadata.image, metadata.icon, ...
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="url" type="string">
    The URL to get metadata for.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="metadata" type="UrlMetadata | undefined">
    The metadata for the URL, or `undefined` if not yet loaded (in non-Suspense
    version).
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the metadata is currently loading.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the metadata.
  </PropertiesListItem>
</PropertiesList>

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

```ts file="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/exports
export {};
```

Here are some example values that might be used.

```ts file="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/exports
export {};
```

### 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](#Typing-your-data) 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.

```tsx file="liveblocks.config.ts" isCollapsable isCollapsed
import { createClient } from "@liveblocks/client";
import { createRoomContext } from "@liveblocks/react";

const client = createClient({
  // publicApiKey: "",
  // authEndpoint: "/api/liveblocks-auth",
});

// Presence represents the properties that exist on every user in the Room
// and that will automatically be kept in sync. Accessible through the
// `user.presence` property. Must be JSON-serializable.
type Presence = {
  // cursor: { x: number, y: number } | null,
  // ...
};

// Optionally, Storage represents the shared document that persists in the
// Room, even after all users leave. Fields under Storage typically are
// LiveList, LiveMap, LiveObject instances, for which updates are
// automatically persisted and synced to all connected clients.
type Storage = {
  // animals: LiveList<string>,
  // ...
};

// Optionally, UserMeta represents static/readonly metadata on each user, as
// provided by your own custom auth back end (if used). Useful for data that
// will not change during a session, like a user’s name or avatar.
// type UserMeta = {
//   id?: string,  // Accessible through `user.id`
//   info?: Json,  // Accessible through `user.info`
// };

// Optionally, the type of custom events broadcast and listened to in this
// room. Use a union for multiple events. Must be JSON-serializable.
// type RoomEvent = {};

// Optionally, when using Comments, ThreadMetadata represents metadata on
// each thread. Can only contain booleans, strings, and numbers.
// export type ThreadMetadata = {
//   pinned: boolean;
//   quote: string;
//   time: number;
// };

export const {
  RoomProvider,
  useMyPresence,
  useStorage,

  // Other hooks
  // ...
} = createRoomContext<
  Presence,
  Storage
  /* UserMeta, RoomEvent, ThreadMetadata */
>(client);
```

To upgrade to Liveblocks 2.0 and the new typing system, follow the
[2.0 migration guide](/docs/platform/upgrading/2.0).

### User [#user-type]

`User` is a type that’s returned by [`useSelf`][], [`useOthers`][], and other
functions. Some of its values are set when
[typing your room](#Typing-your-data), here are some example values:

```ts file="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;
      };
      // +++
    };
  }
}
```

```ts
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();
```

<PropertiesList title="Properties">
  <PropertiesListItem name="connectionId" type="number">
    The connection ID of the User. It is unique and increments with every new
    connection.
  </PropertiesListItem>
  <PropertiesListItem name="id" type={`UserMeta["id"]`}>
    The ID of the User that has been set in the authentication endpoint. Useful
    to get additional information about the connected user.
  </PropertiesListItem>
  <PropertiesListItem name="info" type={`UserMeta["info"]`}>
    Additional user information that has been set in the authentication
    endpoint.
  </PropertiesListItem>
  <PropertiesListItem name="presence" type={`TPresence`}>
    The user’s Presence data.
  </PropertiesListItem>
  <PropertiesListItem name="canWrite" type="boolean">
    `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](/docs/authentication#id-token-room-permissions).
  </PropertiesListItem>
  <PropertiesListItem name="canComment" type="boolean">
    `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](/docs/authentication#id-token-room-permissions).
  </PropertiesListItem>
</PropertiesList>

## Helpers

### shallow

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

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

The default way [selector results](#selectors-return-arbitrary-values) 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):

```tsx
// Comparing arrays
shallow([1, 2, 3], [1, 2, 3]); // true

// Comparison objects
shallow({ 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 [#selectors] [@keywords=["useStorage", "useSelf", "useOthers", "useOther", "useOthersMapped", "useOthersConnectionIds", "selectors", "comparison"]]

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

```tsx file="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);
```

<Banner title="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.

</Banner>

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

- They [receive immutable data](#selectors-receive-immutable-data)
- They [return arbitrary values](#selectors-return-arbitrary-values)
- They [auto-subscribe to updates](#selectors-subscribe-to-updates)

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

### Selectors receive immutable data [#selectors-receive-immutable-data]

The received input to all selector functions is a **read-only** and
**immutable** top level context value that differs for each hook:

- `useStorage((root) => ...)` receives the Storage root
- `useSelf((me) => ...)` receives the current user
- `useOthers((others) => ...)` receives a list of other users in the room
- `useOthersMapped((other) => ...)` receives each individual other user in the
  room
- `useOther(connectionId, (other) => ...)` receives a specific user in the room

For example, suppose you have set up Storage in the typical way by setting
`initialStorage` in your [`RoomProvider`][] to a tree that describes your app’s
data model using `LiveList`, `LiveObject`, and `LiveMap`. The "root" argument
for your selector function, however, will receive **an immutable and read-only
representation** of that Storage tree, consisting of "normal" JavaScript
datastructures. This makes consumption much easier.

```tsx file="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 [#selectors-return-arbitrary-values] [@keywords=["shallow"]]

```tsx file="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](#selectors-subscribe-to-updates)).

#### Examples of stable results

<dl>
  <dt>✅ `(root) => root.animals` is stable</dt>
  <dd>
    Liveblocks guarantees this. All nodes in the Storage tree are stable
    references as long as their contents don’t change.
  </dd>
  <dt>️️⚠️ `(root) => root.animals.map(...)` is not stable</dt>
  <dd>
    Because `.map()` creates a new array instance every time. You’ll need to use
    [`shallow`][] here.
  </dd>
  <dt>✅ `(root) => root.animals.map(...).join(", ")` is stable</dt>
  <dd>
    Because `.join()` ultimately returns a string and all primitive values are
    always stable.
  </dd>
</dl>

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

```tsx
import { shallow } from "@liveblocks/react";

// ❌ Bad - many unnecessary rerenders
const uncheckedItems = useStorage((root) =>
  root.todos.filter((item) => !item.done)
);

// ✅ Great
const 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-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:

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

And the following timeline:

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

[`createclient`]: /docs/api-reference/liveblocks-client#createClient
[`createroomcontext`]: /docs/api-reference/liveblocks-react#createRoomContext
[`livelist`]: /docs/api-reference/liveblocks-client#LiveList
[`livemap`]: /docs/api-reference/liveblocks-client#LiveMap
[`liveobject`]: /docs/api-reference/liveblocks-client#LiveObject
[`lostconnectiontimeout`]:
  /docs/api-reference/liveblocks-client#createClientLostConnectionTimeout
[`room.history`]: /docs/api-reference/liveblocks-client#Room.history
[`roomprovider`]: /docs/api-reference/liveblocks-react#RoomProvider
[`liveblocksprovider`]: /docs/api-reference/liveblocks-react#LiveblocksProvider
[`usemutation`]: /docs/api-reference/liveblocks-react#useMutation
[`usestorage`]: /docs/api-reference/liveblocks-react#useStorage
[`useself`]: /docs/api-reference/liveblocks-react#useSelf
[`useothers`]: /docs/api-reference/liveblocks-react#useOthers
[`useothersmapped`]: /docs/api-reference/liveblocks-react#useOthersMapped
[`useothersconnectionids`]:
  /docs/api-reference/liveblocks-react#useOthersConnectionIds
[`useother`]: /docs/api-reference/liveblocks-react#useOther
[`uselostconnectionlistener`]:
  /docs/api-reference/liveblocks-react#useLostConnectionListener
[`clientsidesuspense`]: /docs/api-reference/liveblocks-react#ClientSideSuspsnse
[`usebroadcastevent`]: /docs/api-reference/liveblocks-react#useBroadcastEvent
[`useupdatemypresence`]:
  /docs/api-reference/liveblocks-react#useUpdateMyPresence
[`usethreads`]: /docs/api-reference/liveblocks-react#useThreads
[`useinboxnotifications`]:
  /docs/api-reference/liveblocks-react#useInboxNotifications
[`usemypresence`]: /docs/api-reference/liveblocks-react#useMyPresence
[`usesyncstatus`]: /docs/api-reference/liveblocks-react#useSyncStatus
[`useerrorlistener`]: /docs/api-reference/liveblocks-react#useErrorListener
[`room`]: /docs/api-reference/liveblocks-client#Room
[`shallow`]: /docs/api-reference/liveblocks-react#shallow
[`resolveusers`]: /docs/api-reference/liveblocks-client#resolveUsers
[`resolveroomsinfo`]: /docs/api-reference/liveblocks-client#resolveRoomsInfo
[selector]: /docs/api-reference/liveblocks-react#selectors
[how selectors work]: /docs/api-reference/liveblocks-react#selectors
[suspense version]: /docs/api-reference/liveblocks-react#Suspense
[connection status example]:
  https://liveblocks.io/examples/connection-status/nextjs
[`atob`]: https://developer.mozilla.org/en-US/docs/Web/API/atob
[`base-64`]: https://www.npmjs.com/package/base-64
[`websocket`]: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
[`ws`]: https://www.npmjs.com/package/ws
[`fetch`]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
[`node-fetch`]: https://npmjs.com/package/node-fetch

---

For an overview of all available documentation, see [/llms.txt](/llms.txt).
