# Livestream chat

Livestream chat sits next to a video stream: thousands of viewers, a fast flow of short messages, and a list that has to stay smooth for hours. This page covers which channel type to use and how to keep memory and rendering bounded. It also covers unread counts without read events, and which features to turn off.

## What users get

>
> **Info:** **Example**
>
> The user opens a concert stream. Next to the video, the chat shows the newest messages and follows them as they arrive, several a second. Messages from many people scroll by in a compact list.
>
> The user scrolls up to read a question. New messages keep arriving below, and a button offers to take them back to the latest one. When they return to the bottom, the list follows the newest messages again.
>
> An hour later, the browser still holds only the newest few hundred messages, not every message of the stream.
>

## How it works across the layers

### Server: the livestream channel type

Stream has a built-in `livestream` channel type. Users don't have to be members to read and write. Any user with a valid token can watch the channel and post. Its `url_enrichment` setting is off by default.

Check the type's Read events setting (`read_events`) and turn it off. [Livestream best practices](https://getstream.io/chat/docs/javascript/livestream-best-practices/) recommends turning off read events, typing events and connect events on the type. It also explains the server's own message throttling. [Channel types](https://getstream.io/chat/docs/javascript/channel-features/) lists every setting.

The channel type's settings decide what the client may do. A setting that is off on the server is off in the client too, whatever `client.config` says. See [Configuration](https://getstream.io/chat/docs/sdk/react/state/state-and-entities/configuration/).

### State: the message paginator

The channel's messages live in `channel.messagePaginator`. Two of its settings matter for a busy channel:

- `stateThrottleMs` limits how often the paginator publishes its `items`, and with them how often the list renders. It is 500 ms by default, so a burst of messages causes about two renders a second. Messages the user sends show at once.
- `maxLoadedItems` caps how many messages the paginator keeps. It is off by default. See [Capping the messages](#capping-the-messages).

[Message paginators](https://getstream.io/chat/docs/sdk/react/state/loading-data/message-paginators/#throttling-and-the-memory-cap) explains both.

### State: read state without read events

With `read_events` off, the server keeps no read state for the channel, so nothing counts as unread. [Turning read events off](https://getstream.io/chat/docs/sdk/react/state/channels-and-threads/read-state-and-unread-counts/#turning-read-events-off) in Read state and unread counts explains what else changes. [Counting unread messages locally](#counting-unread-messages-locally) shows how to bring back an unread count that lives only on the device.

### UI: the virtualized message list

`VirtualizedMessageList` renders only the messages near the visible area, so the page holds a few dozen message elements however many messages are loaded. `MessageList` renders every loaded message. In a livestream, use `VirtualizedMessageList`.

Its defaults suit a livestream. Unlike `<MessageList/>`, it doesn't show date separators, doesn't group consecutive messages by sender, and doesn't show a typing indicator unless you register one. [MessageList and VirtualizedMessageList](https://getstream.io/chat/docs/sdk/react/ui/components/message-list-ui/#messagelist-and-virtualizedmessagelist) in Message list UI compares the two lists and their defaults.

## Turning it on or off

### Opening the channel

`Channel` doesn't load the channel. Watch it first, then pass it in. This app opens one livestream channel with the virtualized list:

```tsx title="src/App.tsx"
import { useEffect, useState } from "react";
import type { Channel as StreamChannel } from "stream-chat";
import {
  Channel,
  Chat,
  MessageComposer,
  useCreateChatClient,
  VirtualizedMessageList,
} from "stream-chat-react";
import "stream-chat-react/dist/css/index.css";

const apiKey = "YOUR_API_KEY";
const userId = "YOUR_USER_ID";
const userToken = "YOUR_USER_TOKEN";

export const App = () => {
  const client = useCreateChatClient({
    apiKey,
    tokenOrProvider: userToken,
    userData: { id: userId },
  });
  const [channel, setChannel] = useState<StreamChannel>();

  useEffect(() => {
    if (!client) return;
    let cancelled = false;

    client
      .channel("livestream", "concert")
      .watch({ messages: { limit: 30 } })
      .then((watchedChannel) => {
        if (!cancelled) setChannel(watchedChannel);
      })
      .catch((error) => console.error("Could not watch the channel", error));

    return () => {
      cancelled = true;
      setChannel(undefined);
    };
  }, [client]);

  if (!client || !channel) return <div>Loading...</div>;

  return (
    <Chat client={client}>
      <Channel channel={channel}>
        <VirtualizedMessageList stickToBottomScrollBehavior="auto" />
        <MessageComposer />
      </Channel>
    </Chat>
  );
};
```

- `Channel.watch()` loads the channel and subscribes to its events. It doesn't subscribe to the presence of the channel's users unless you pass `presence: true`, which a livestream doesn't need.
- `messages.limit` sizes the first page only. Later pages, loaded as the user scrolls up, use the paginator's `pageSize`.
- `stickToBottomScrollBehavior="auto"` jumps to each new message instead of scrolling smoothly to it. Smooth scrolling can't keep up with several messages a second.

### Capping the messages

Set `maxLoadedItems` on the channels' message paginators. Once the newest messages loaded exceed the cap, each new message drops the oldest one. To cap every channel, set it in `client.config`, here through `useCreateChatClient`:

```tsx
import { useCreateChatClient } from "stream-chat-react";

export const useLivestreamClient = (
  apiKey: string,
  userId: string,
  userToken: string,
) =>
  useCreateChatClient({
    apiKey,
    tokenOrProvider: userToken,
    userData: { id: userId },
    config: {
      channel: { messagePaginator: { maxLoadedItems: 500 } },
    },
  });
```

To cap only livestream channels, set it from a setup function. It runs for every channel, so it can check the channel's type:

```ts
client.config.setSetupFunction("channel", ({ channel }) => {
  if (channel.type !== "livestream") return;
  channel.messagePaginator.updateConfig({ maxLoadedItems: 500 });
});
```

There is one `channel` setup function, so this one replaces any you set before; merge them into one. [Setup functions](https://getstream.io/chat/docs/sdk/react/state/state-and-entities/configuration/#setup-functions) in Configuration explains how they run.

[Throttling and the memory cap](https://getstream.io/chat/docs/sdk/react/state/loading-data/message-paginators/#throttling-and-the-memory-cap) in Message paginators explains how the cap behaves: which messages it never drops, and when it applies.

#### Scrolling back

>
> **Info:** **Example**
>
> The cap is 500, and a page is 100 messages by default. The user scrolls up and loads two older pages, so the paginator holds 700 messages. The next new message drops the 201 oldest, which are the ones the user is reading. The list jumps.
>

To avoid this, pause the cap with `MessagePaginator.setPruningSuspended()`. The SDK's lists don't do it for you. A prune drops the oldest loaded messages, so a pause is needed only while the user reads near them. This component pauses the cap whenever the user has scrolled back, which is simpler but keeps more messages in memory. It keeps the cap while the tab is in the background. Render it inside `<Channel/>`, next to the list:

```tsx
import { useEffect } from "react";
import { useMessagePaginator, useStateStore } from "stream-chat-react";

const liveViewSelector = ({ isViewingLive }: { isViewingLive: boolean }) => ({
  isViewingLive,
});

export const PausePruningWhileReadingBack = () => {
  const messagePaginator = useMessagePaginator();
  const { isViewingLive } = useStateStore(
    messagePaginator.liveViewState,
    liveViewSelector,
  );

  useEffect(() => {
    // A hidden tab isn't viewing live either, but it should keep the cap.
    const update = () =>
      messagePaginator.setPruningSuspended(!isViewingLive && !document.hidden);

    update();
    document.addEventListener("visibilitychange", update);
    return () => {
      document.removeEventListener("visibilitychange", update);
      messagePaginator.setPruningSuspended(false);
    };
  }, [isViewingLive, messagePaginator]);

  return null;
};
```

The list sets `isViewingLive` in `channel.messagePaginator.liveViewState`. It is `true` while the user is at the bottom, the newest messages are loaded and the browser tab is visible. A hidden tab counts as not viewing live, so the component also checks `document.hidden`. It pauses the cap only while the tab is visible and the user is away from the newest messages.

While the cap is paused, the paginator keeps every message. When the user is back at the newest messages, or the tab goes to the background, the next new message drops everything over the cap at once. A user who leaves the tab while scrolled back can return to a shorter list.

#### Jumping to a message

A jump, for example to a quoted message or a pinned announcement, shows the messages around the target instead of the newest ones. The cap applies to neither the messages around the target nor the newest messages while they're off screen, so the newest messages keep growing as messages arrive. When the user jumps back to the latest message, the next new message brings the newest messages back down to the cap. [Throttling and the memory cap](https://getstream.io/chat/docs/sdk/react/state/loading-data/message-paginators/#throttling-and-the-memory-cap) in Message paginators explains when the cap applies.

### Counting unread messages locally

Turn on `channel.readEvents.localUnreadCountEnabled` in `client.config` to count unread messages on this device, without the server. [Turning read events off](https://getstream.io/chat/docs/sdk/react/state/channels-and-threads/read-state-and-unread-counts/#turning-read-events-off) in Read state and unread counts explains what the local count counts, how it resets and who sees it.

#### What the user sees

Without the local count, the UI has no unread count for the channel. Its item in a channel list has no unread badge. When messages arrive while the user is scrolled up, the message list shows "New Messages!" without a number.

With the local count:

- The channel's item in a channel list shows the unread count, read from `channel.state.read`.
- The message list's notification counts the messages that arrived while the user was scrolled up, for example "12 new messages".

In both cases the user can't mark a message unread. That action needs the `read-events` capability, which channels without read events don't give.

### Turning off typing, presence and delivery receipts

Typing events are per channel type. Turn off `typing_events` on the `livestream` type on the server, and the client sends and shows none.

In the client, a `messageComposer` setup function can stop the composers of livestream channels from sending typing events:

```ts
client.config.setSetupFunction("messageComposer", ({ composer }) => {
  if (composer.channel.type !== "livestream") return;
  composer.updateConfig({ text: { publishTypingEvents: false } });
});
```

To turn typing off in this client for every channel instead, set `channel.typingEvents.enabled: false` in `client.config`. [Typing indicators](https://getstream.io/chat/docs/sdk/react/features/messaging/typing-indicators/#turning-it-on-or-off) explains the server and client settings.

For presence, watch the channel without `presence: true`, which is the default, and turn off `connect_events` on the channel type.

Delivery receipts are per channel type too. When the type's `delivery_events` setting is on, every viewer reports delivery of the newest message through `client.messageDeliveryReporter`, about once a second while messages arrive. Turn off `delivery_events` on the `livestream` type, or set `channel.deliveryEvents.enabled: false` in `client.config` to stop this client from reporting delivery. [Message delivery and read status](https://getstream.io/chat/docs/javascript/message-delivery-and-read-status/#enabling-delivery-receipts) explains the type setting.

## Customizing

### A compact list

`VirtualizedMessageList` takes props that make the list denser and cheaper to render:

```tsx
import { VirtualizedMessageList } from "stream-chat-react";

export const LivestreamMessageList = () => (
  <VirtualizedMessageList
    additionalVirtuosoProps={{ defaultItemHeight: 40 }}
    hideDeletedMessages
    showAvatar={false}
    stickToBottomScrollBehavior="auto"
  />
);
```

- `showAvatar={false}` hides every avatar. It also takes `"incoming"`, the default, or `"outgoing"`.
- `hideDeletedMessages` leaves deleted messages out, so moderated messages disappear instead of leaving a placeholder.
- `additionalVirtuosoProps` passes options to `react-virtuoso`. `defaultItemHeight` is the height the list assumes for a message it hasn't measured yet. A value close to a real message keeps the scroll bar steady.

Keep a custom message UI light. The list mounts and unmounts messages as the user scrolls, and measures each one. [VirtualizedMessageList](https://getstream.io/chat/docs/sdk/react/component-reference/message-list/virtualizedmessagelist/) lists every prop.

### Showing the viewer count

`channel.state` holds `watcherCount`, the number of users watching the channel. The server sends it with the channel query, and events from the channel can carry an update. `ChannelHeader` shows it as "N online" next to the member count, but only when the channel has members. Viewers of a livestream usually aren't members. This badge shows the count on its own:

```tsx
import type { ChannelStateData } from "stream-chat";
import { useChannel, useStateStore } from "stream-chat-react";

const watcherCountSelector = ({ watcherCount }: ChannelStateData) => ({
  watcherCount,
});

export const ViewerCount = () => {
  const channel = useChannel();
  const { watcherCount } = useStateStore(channel.state, watcherCountSelector);

  return <span className="viewer-count">{watcherCount} watching</span>;
};
```

Render it anywhere inside `Channel`.

### Announcements as pinned messages

Pin a message to keep it reachable, for example a link or the rules of the chat. The channel's pinned messages are in `channel.pinnedMessagesPaginator`, and jumping to one works as described in [Jumping to a message](#jumping-to-a-message).

The Pin and Unpin actions show only to users with the `pin-message` capability, so viewers who may not pin don't see them. [Pinned messages](https://getstream.io/chat/docs/sdk/react/features/messaging/pinned-messages/#turning-it-on-or-off) explains who has it.

### Slow mode

Slow mode makes each user wait between messages, which keeps a busy chat readable. Turn it on with `Channel.enableSlowMode(seconds)`. Moderators and admins don't wait. See [Slow mode](https://getstream.io/chat/docs/sdk/react/features/channels/slow-mode/).

### Moderation

From the message actions, viewers can flag a message and mute or block its author, and moderators can delete any message. The Flag, Mute and Delete actions show only for users with the matching capability: `flag-message`, `mute-channel`, and `delete-any-message` or `delete-own-message`. Block has no capability check, so every viewer sees it on other users' messages. [Flagging, muting, blocking and bans](https://getstream.io/chat/docs/sdk/react/features/moderation-and-safety/flagging-muting-blocking-and-bans/) covers them. Block lists and automated moderation are set on the server, see [Moderation](https://getstream.io/chat/docs/javascript/moderation/).

## Reference

- [VirtualizedMessageList](https://getstream.io/chat/docs/sdk/react/component-reference/message-list/virtualizedmessagelist/): every prop of the list.
- [Message paginator](https://getstream.io/chat/docs/sdk/react/reference/state/message-paginator/): `maxLoadedItems`, `stateThrottleMs`, `setPruningSuspended` and `liveViewState`.
- [Channel and channel state](https://getstream.io/chat/docs/sdk/react/reference/state/channel-and-channel-state/): `watcherCount`, `markReadLocally` and `enableSlowMode`.
- [Configuration tree](https://getstream.io/chat/docs/sdk/react/reference/state/configuration-tree/): `channel.messagePaginator`, `channel.readEvents` and `client.messageDelivery`.
- [Livestream best practices](https://getstream.io/chat/docs/javascript/livestream-best-practices/): the server side of a livestream.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/features/channels/livestream-chat/](https://getstream.io/chat/docs/sdk/react/features/channels/livestream-chat/).