Info:
This is release candidate documentation for Stream Chat React SDK v15. For the latest stable version, see the latest version (v14).

Quick start

This page builds a chat app in four steps. Each step is a complete src/App.tsx. For a guided version, follow the React Chat tutorial.

Before you start:

  • Install stream-chat and stream-chat-react as described in Installation.
  • Create a Stream Chat app and generate a token for a user. The examples use its API key, the user's ID and the token in place of YOUR_API_KEY, YOUR_USER_ID and YOUR_USER_TOKEN.

Create the client

src/App.tsx
import { Chat, useCreateChatClient } 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";

const App = () => {
  const client = useCreateChatClient({
    apiKey,
    tokenOrProvider: userToken,
    userData: { id: userId },
  });

  if (!client) return <div>Connecting...</div>;

  return <Chat client={client}>The client is connected.</Chat>;
};

export default App;

useCreateChatClient creates a StreamChat client and connects the user. It returns null until the connection is open, and then the client. When the component unmounts, it disconnects the user.

Chat gives the client to every SDK component inside it. Your own components read it with useChatContext().

The hook creates a new client when apiKey, the user's id or tokenOrProvider changes. So a token provider function must not be re-created on every render: memoize it with useCallback, or declare it outside the component.

To configure the client and the objects it creates, pass a config option to useCreateChatClient. It takes the same tree as client.config.set(), and the hook reads it once, on the first render. This one turns on message drafts:

src/App.tsx
import { Chat, useCreateChatClient } 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";

const App = () => {
  const client = useCreateChatClient({
    apiKey,
    tokenOrProvider: userToken,
    config: { messageComposer: { drafts: { enabled: true } } },
    userData: { id: userId },
  });

  if (!client) return <div>Connecting...</div>;

  return <Chat client={client}>The client is connected.</Chat>;
};

export default App;

The settings and their defaults are on Configuration.

Show one channel

A single channel needs no layout. Get the channel from the client, watch it, and pass it to Channel:

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

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

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

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

    // Watching loads the channel and subscribes to its events. The server creates
    // the channel if it doesn't exist yet.
    client
      .channel("messaging", "react-quick-start", {
        members: [{ user_id: userId }],
      })
      .watch()
      .then((watchedChannel) => {
        if (!cancelled) setChannel(watchedChannel);
      })
      .catch((error) => console.error("Could not watch the channel", error));

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

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

  return (
    <Chat client={client}>
      <Channel channel={channel}>
        <ChannelHeader />
        <MessageList />
        <MessageComposer />
      </Channel>
    </Chat>
  );
};

export default App;
src/layout.css
html,
body,
#root {
  height: 100%;
}

body {
  margin: 0;
}

Here is what happens:

  1. client.channel("messaging", "react-quick-start", ...) returns the object for that channel. The client keeps one object per channel, so a second call returns the same one.
  2. Channel.watch() watches the channel: it loads the messages, members and read state, and subscribes to the channel's events. The server creates the channel the first time. Calling it again is safe, even from several places at once: it sends a request only when the channel isn't loaded yet, or when you pass options it wasn't watched with. So it's safe in code that can run more than once, such as this effect. The promise resolves with the channel object to use from then on. See watch and query.
  3. Channel gives the channel to the components inside it. It loads nothing itself, so the app renders it once the channel is watched. A component inside reads the channel with useChannel().

The channel type, messaging, decides which features and permissions the channel has. See channel types.

layout.css makes the page fill the window, so the channel can fill the page. #root is the element the app renders into.

Without a layout, a thread has nowhere to open, so the reply button on a message does nothing. The next steps add ChatView, which places channels and threads.

Add a channel list with ChatView

Channels are queried from server and kept in lists matching the filters and a sort order by controllers called ChannelPaginator. Registered on client.channelManager, the lists are kept up to date as messages and other events arrive. In the sample below, ChannelNavigation renders a ChannelList for single registered paginator. The paginator loads its first page when ChannelList mounts.

Clicking a channel in the list opens it. Where it opens is up to the layout, ChatView from stream-chat-react/slot-layout:

src/App.tsx
import { useEffect } from "react";
import { ChannelPaginator } from "stream-chat";
import type { ChannelFilters, SortParamRequest } from "stream-chat";
import {
  ChannelHeader,
  ChannelNavigation,
  Chat,
  MessageComposer,
  MessageList,
  useCreateChatClient,
} from "stream-chat-react";
import { ChannelSlot, ChatView } from "stream-chat-react/slot-layout";
import type { LayoutDescriptor } from "stream-chat-react/slot-layout";
import "stream-chat-react/dist/css/index.css";
import "./layout.css";

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

// The user's messaging channels, latest message first.
const filters: ChannelFilters = {
  members: { $in: [userId] },
  type: "messaging",
};
const sort: SortParamRequest[] = [{ direction: -1, field: "last_message_at" }];

// The channels view has two slots: "channel" for the channel messages, and "thread"
// for a message replies opened from it. The variable layouts is declared outside
// the component, so ChatView gets the same array on every render.
const layouts: LayoutDescriptor[] = [
  { id: "channels", slots: ["channel", "thread"] },
];

const ChannelsView = () => (
  <>
    {/* The list header, the search bar and a ChannelList for each registered paginator */}
    <ChannelNavigation />
    <ChannelSlot slot="channel">
      <ChannelHeader />
      <MessageList />
      <MessageComposer />
    </ChannelSlot>
  </>
);

const App = () => {
  const client = useCreateChatClient({
    apiKey,
    tokenOrProvider: userToken,
    userData: { id: userId },
  });

  // Registers the list on the channel manager, which keeps it up to date.
  useEffect(() => {
    if (!client) return;
    const paginator = new ChannelPaginator({
      client,
      filters,
      id: "my-channels",
      sort,
    });
    client.channelManager.insertPaginator({ paginator });
    return () => {
      client.channelManager.removePaginator(paginator);
    };
  }, [client]);

  if (!client) return <div>Connecting...</div>;

  return (
    <Chat client={client}>
      <ChatView layouts={layouts} views={{ channels: <ChannelsView /> }} />
    </Chat>
  );
};

export default App;
src/layout.css
html,
body,
#root {
  height: 100%;
}

body {
  margin: 0;
}

.str-chat {
  --str-chat__channel-list-width: 320px;
}

Here is what happens when the user clicks a channel:

  1. ChannelNavigation renders a stack of the channel list header, the search bar and one ChannelList for each paginator registered on the channel manager. The effect registers one paginator, "my-channels", so there is one list. A row in the list calls openChannel(channel) from useWorkspaceNavigation() when clicked.
  2. ChatView puts the channel in a slot of the channels view. layouts declares the slots: the slot called "channel" gets populated with the data of the channel selected from the list. The ChannelSlot is the slot component that declares itself as a target slot via slot property (slot='channel').

The thread slot is where a thread opens. The next step renders it. Declaring it now keeps a thread from opening in the channel slot, in place of the channel.

The CSS variable gives the channel list a fixed width, and the channel takes the rest. On screens narrower than 768px, the SDK's stylesheet hides the channel list, and an app shows it there itself, for example as an overlay. Try this step in a wider window.

Add threads

Threads open in two places: beside the channel, when the user opens the replies to a message, and in a threads view that lists the user's threads. This step adds both:

src/App.tsx
import { useEffect } from "react";
import { ChannelPaginator } from "stream-chat";
import type { ChannelFilters, SortParamRequest } from "stream-chat";
import {
  ChannelHeader,
  ChannelNavigation,
  Chat,
  MessageComposer,
  MessageList,
  ThreadHeader,
  ThreadList,
  useCreateChatClient,
} from "stream-chat-react";
import {
  ChannelSlot,
  ChatView,
  ThreadSlot,
} from "stream-chat-react/slot-layout";
import type { LayoutDescriptor } from "stream-chat-react/slot-layout";
import "stream-chat-react/dist/css/index.css";
import "./layout.css";

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

const filters: ChannelFilters = {
  members: { $in: [userId] },
  type: "messaging",
};
const sort: SortParamRequest[] = [{ direction: -1, field: "last_message_at" }];

// The channels view has a slot for the open channel and one for a thread opened
// from it. The threads view has one slot, for the thread picked in the list.
const layouts: LayoutDescriptor[] = [
  { id: "channels", slots: ["channel", "thread"] },
  { id: "threads", slots: ["thread"] },
];

// The thread in the current view's "thread" slot, or nothing while none is open.
const ThreadPanel = () => (
  <ThreadSlot slot="thread">
    <ThreadHeader />
    <MessageList />
    <MessageComposer />
  </ThreadSlot>
);

const ChannelsView = () => (
  <>
    <ChatView.Selector />

    <ChannelNavigation />

    <ChannelSlot slot="channel">
      <ChannelHeader />
      <MessageList />
      <MessageComposer />
    </ChannelSlot>

    <ThreadPanel />
  </>
);

const ThreadsView = () => (
  <>
    <ChatView.Selector />
    <ThreadList />
    <ThreadPanel />
  </>
);

const App = () => {
  const client = useCreateChatClient({
    apiKey,
    tokenOrProvider: userToken,
    userData: { id: userId },
  });

  useEffect(() => {
    if (!client) return;
    const paginator = new ChannelPaginator({
      client,
      filters,
      id: "my-channels",
      sort,
    });
    client.channelManager.insertPaginator({ paginator });
    return () => {
      client.channelManager.removePaginator(paginator);
    };
  }, [client]);

  if (!client) return <div>Connecting...</div>;

  return (
    <Chat client={client}>
      <ChatView
        layouts={layouts}
        views={{ channels: <ChannelsView />, threads: <ThreadsView /> }}
      />
    </Chat>
  );
};

export default App;

Give the thread list a fixed width too:

src/layout.css
html,
body,
#root {
  height: 100%;
}

body {
  margin: 0;
}

.str-chat {
  --str-chat__channel-list-width: 320px;
  --str-chat__thread-list-width: 320px;
}

What changed:

  • ThreadSlot captures all the requests to open slot "thread" and renders the thread UI. While the slot is empty, it renders nothing. It provides Thread instance to all the child components.
  • The threads view has its own thread slot and thread listing through ThreadList.
  • ChatView.Selector renders the buttons that switch between the channels view and the threads view. The threads button shows the number of unread threads.

The emoji picker is optional. It comes from stream-chat-react/emojis and needs the emoji-mart and @emoji-mart/data packages. See Emoji.

Next steps