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-chatandstream-chat-reactas 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_IDandYOUR_USER_TOKEN.
Create the client
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:
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:
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;html,
body,
#root {
height: 100%;
}
body {
margin: 0;
}Here is what happens:
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.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.Channelgives 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 withuseChannel().
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:
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;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:
ChannelNavigationrenders a stack of the channel list header, the search bar and oneChannelListfor each paginator registered on the channel manager. The effect registers one paginator,"my-channels", so there is one list. A row in the list callsopenChannel(channel)fromuseWorkspaceNavigation()when clicked.ChatViewputs the channel in a slot of the channels view.layoutsdeclares the slots: the slot called"channel"gets populated with the data of the channel selected from the list. TheChannelSlotis the slot component that declares itself as a target slot viaslotproperty (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:
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:
html,
body,
#root {
height: 100%;
}
body {
margin: 0;
}
.str-chat {
--str-chat__channel-list-width: 320px;
--str-chat__thread-list-width: 320px;
}What changed:
ThreadSlotcaptures all the requests to open slot"thread"and renders the thread UI. While the slot is empty, it renders nothing. It providesThreadinstance to all the child components.- The
threadsview has its ownthreadslot and thread listing throughThreadList. ChatView.Selectorrenders 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.