Stream's Chat React messaging SDK component library enables teams of all sizes to build a fully functioning chat experience, with support for rich messages, reactions, threads, image uploads, videos, and more. The SDK ships with a redesigned interface, a more cohesive design system, and clearer customization surfaces so you can get to a polished chat experience quickly and still make it feel like your product.
This tutorial has been tested with the following versions:
- React ^19
- Stream Chat SDK ^14
If you prefer, you can use this sandbox environment which is configured with the right versions for you to get started.
In this tutorial, we will use the Stream's Chat SDK for React to:
- Set up a polished chat application quickly with Stream's React UI components.
- List available channels using the ChannelList component.
- Customize the look and feel of the UI with the SDK's design system.
- Replace SDK-owned UI surfaces through the current
WithComponentscustomization model. - Customize the Attachment component to support custom attachment types.
Project Setup and Installation
The easiest way to build a Stream Chat React application in JavaScript from this tutorial is to create a new project using Vite.
Vite allows you to create a boilerplate React application that you can run locally with just a few simple commands.
Create a new React project called chat-example with Vite using the TypeScript template:
123npm create vite chat-example -- --template react-ts cd chat-example npm i stream-chat stream-chat-react
123yarn create vite chat-example --template react-ts cd chat-example yarn add stream-chat stream-chat-react
To make the tutorial as easy as possible, each of our code samples in this tutorial comes with generated credentials for you to pick up and use. These credentials consist of:
apiKey- an API key that is used to identify your Stream application by our serversuserIdanduserToken- authorization information of the current chat useruserName- optional, used as a display name of the current chat user
Now if you already have and wish to use your own Stream application, you can certainly do so - feel free to use our token (JWT) generator utility to easily generate authorization token for your user. Learn more about authentication in our Tokens & Authentication documentation.
Client Setup
Before we begin working with the chat UI components, we'll need to set up a StreamChat client instance which abstracts API calls into methods, handles state and real-time events.
To make the instantiation and connection handling easier, we've prepared a simple hook (useCreateChatClient) that you can use in your application.
Let's begin by replacing the contents of the generated src/App.tsx file with this code snippet:
Note: Make sure you use the
useCreateChatClienthook only once per application. If you need the client instance somewhere down in the component tree use theuseChatContexthook (exported by thestream-chat-reactSDK) to access it.
123456789101112131415161718192021import { Chat, useCreateChatClient } from 'stream-chat-react'; // your Stream app information const apiKey = 'REPLACE_WITH_API_KEY'; const userId = 'REPLACE_WITH_USER_ID'; const userName = 'REPLACE_WITH_USER_NAME'; const userToken = 'REPLACE_WITH_USER_TOKEN'; const App = () => { const client = useCreateChatClient({ apiKey, tokenOrProvider: userToken, userData: { id: userId, name: userName }, }); if (!client) return <div>Setting up client & connection...</div>; return <Chat client={client}>Chat with client is ready!</Chat>; }; export default App;
Note: The client has to have a connection established before you can pass it down to the
Chatcomponent otherwise it won't work.
If this hook somehow gets in a way, you can certainly adjust the client setup to your liking, see useCreateChatClient source for inspiration.
Start your application with yarn dev or npm run dev and navigate to the site generated by this command. In the network tab of the developer tools, you should be able to see a secure websocket connection to our servers if everything was set up correctly.
Initial Core Component Setup
Now that the client connection has been established, it's time to make the application interactable. To do this, we'll create a base setup which we'll be modifying and expanding on later in the tutorial. This initial setup is built using these core components:
- Chat
- Channel
- ChannelHeader
- MessageComposer (with EmojiPicker and emoji autocomplete)
- MessageList
- Thread
- Window
Let's extend our src/App.tsx code with this new setup:
Note: When you copy the following code into your editor, your editor may show you red squiggly lines under
nameandimage. That is expected, and we will add the code for that in a bit.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354import { useState, useEffect } from 'react'; import { type User, Channel as StreamChannel } from 'stream-chat'; import { useCreateChatClient, Chat, Channel, ChannelHeader, MessageComposer, MessageList, Thread, Window } from 'stream-chat-react'; import 'stream-chat-react/dist/css/index.css'; const apiKey = 'REPLACE_WITH_API_KEY'; const userId = 'REPLACE_WITH_USER_ID'; const userName = 'REPLACE_WITH_USER_NAME'; const userToken = 'REPLACE_WITH_USER_TOKEN'; const user: User = { id: userId, name: userName, image: `https://getstream.io/random_png/?name=${userName}`, }; const App = () => { const [channel, setChannel] = useState<StreamChannel>(); const client = useCreateChatClient({ apiKey, tokenOrProvider: userToken, userData: user, }); useEffect(() => { if (!client) return; const channel = client.channel('messaging', 'custom_channel_id', { image: 'https://getstream.io/random_png/?name=react', name: 'Talk about React', members: [userId], }); setChannel(channel); }, [client]); if (!client) return <div>Setting up client & connection...</div>; return ( <Chat client={client}> <Channel channel={channel}> <Window> <ChannelHeader /> <MessageList /> <MessageComposer /> </Window> <Thread /> </Channel> </Chat> ); }; export default App;
Custom Entity Properties
The Stream API allows you to store custom data on entities (channel, message, user, etc.). For example, the image on Channel is a custom property.
For proper type-checking and better auto-complete when using TypeScript, you need to add a d.ts file to your project (e.g.: src/stream-chat.d.ts) and declare that your channel instances will carry the image custom property:
1234567import { DefaultChannelData } from 'stream-chat-react'; declare module 'stream-chat' { interface CustomChannelData extends DefaultChannelData { image?: string; } }
With this declaration, we have extended the Channel type to include the image property. Feel free to add more properties to the CustomChannelData interface as needed.
Adding Layout Styling
You might've noticed that the application does not look very appealing at the moment.
The SDK ships with polished component styling, but you still control the page layout.
For now, let's create a CSS file (src/layout.css) with a simple two-pane layout:
123456789101112131415161718192021html, body, #root { height: 100%; } body { margin: 0; } #root { display: flex; } .str-chat__channel-list { width: 30%; } .str-chat__channel { width: 100%; } .str-chat__thread { width: 45%; }
And then we'll import it in src/App.tsx right below stream-chat-react CSS import:
123// other imports import 'stream-chat-react/dist/css/index.css'; import './layout.css';
Note: Be sure to get rid of the Vite's default
src/index.cssimports to make sure your styling isn't getting messed up with unwanted rules.
Out-of-the-box Features
The Chat and Channel components are React context providers that pass a variety of values to their children, including UI components, channel state data, and messaging functions.
Note how you create a channel with the channel method available on the StreamChat client instance.
The first argument is the channel type messaging; the second argument is optional and specifies either the channel id.
The channel type determines the enabled features and permissions associated with this channel. The channel id is a unique reference to this specific channel.
Once you have the app running, you’ll notice the following out-of-the-box features:
- User online presence
- Typing indicators
- Message status indicators and failure states
- User role configuration
- Emoji support (opt-in)
- Unread indicators and read-state UI
- Threading and message replies
- Message reactions
- URL previews (send a YouTube link to see this in action)
- File uploads and previews
- Video playback
- Autocomplete-enabled search on users, emojis (opt-in), and commands
- Slash commands such as /giphy (custom commands are also supported)
- AI-powered spam and profanity moderation

Add a Channel List
The ChannelList component displays a list of channel previews. It internally loads the data about the channels relevant to our user.
The channels are loaded according to the criteria specified through its props. These props are:
filter: applies a filter to the query which loads the channels, minimal filter should include channel type and membership to load channels related only to the connected user (see example)sort: applies a sorting criteria to the channels selected by the filter, ideally we want to sort the channels by the time of the last messageoptions: applies some additional options to the query, in this case - limit the number of channels we load to 10
Note: You can read more in the ChannelList documentation.
Let's adjust our code to render a list of channels through the use of the ChannelList component:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263import type { User, ChannelSort, ChannelFilters, ChannelOptions } from 'stream-chat'; import { useCreateChatClient, Chat, Channel, ChannelHeader, ChannelList, MessageComposer, MessageList, Thread, Window, } from 'stream-chat-react'; import 'stream-chat-react/dist/css/index.css'; import './layout.css'; const apiKey = 'REPLACE_WITH_API_KEY'; const userId = 'REPLACE_WITH_USER_ID'; const userName = 'REPLACE_WITH_USER_NAME'; const userToken = 'REPLACE_WITH_USER_TOKEN'; const user: User = { id: userId, name: userName, image: `https://getstream.io/random_png/?name=${userName}`, }; const sort: ChannelSort = { last_message_at: -1 }; const filters: ChannelFilters = { type: 'messaging', members: { $in: [userId] }, }; const options: ChannelOptions = { limit: 10, }; const App = () => { const client = useCreateChatClient({ apiKey, tokenOrProvider: userToken, userData: user, }); if (!client) return <div>Setting up client & connection...</div>; return ( <Chat client={client}> <ChannelList filters={filters} sort={sort} options={options} /> <Channel> <Window> <ChannelHeader /> <MessageList /> <MessageComposer /> </Window> <Thread /> </Channel> </Chat> ); }; export default App;
You'll notice that we've removed channel instantiation part as the channels are now handled by the ChannelList component which will set the first channel in the list as active automatically.
This does not mean that you cannot use the previous code sample to programmatically set your channels anymore - it's just unnecessary in this specific example.
This detail is important: when you want the ChannelList to control the active channel, render <Channel> without a fixed channel prop.
Once you pass a specific channel instance to Channel, that instance becomes the source of truth instead of the selected channel from the list.
Theming
Theming in the React SDK is done through CSS variables and design tokens. The default theme already looks polished, but you can quickly brand it by overriding a small set of global variables. Let's take a look at how to build a simple custom theme.
First, let's move our stream-chat-react CSS import from src/App.tsx to src/layout.css and load it into a stream css-layer.
We'll put our custom tokens in a stream-overrides layer so the browser applies our brand styles after the SDK defaults.
Our src/layout.css should now look something like this:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253@layer stream, stream-overrides; @import 'stream-chat-react/css/index.css' layer(stream); @layer stream-overrides { .custom-theme { /* Accent color — used by mentions, read receipts, attachment actions, focus states, etc. */ --accent-primary: #0d47a1; /* Message bubble colors */ --chat-bg-outgoing: #1e3a8a; --chat-bg-attachment-outgoing: #0d47a1; --chat-bg-incoming: #dbeafe; --chat-text-outgoing: #ffffff; --chat-reply-indicator-outgoing: #93c5fd; /* Link colors (inside bubbles and elsewhere) */ --text-link: #1e40af; --chat-text-link: #93c5fd; /* Panel backgrounds */ --background-core-elevation-1: #dbeafe; /* channel list and surrounding panels */ --background-core-app: #c7dafc; /* message list background */ /* Focus ring */ --border-utility-focused: #1e40af; /* Radii — the SDK uses --radius-max / --button-radius-full for pill shapes */ --radius-max: 8px; --button-radius-full: 6px; } } html, body, #root { height: 100%; } body { margin: 0; } #root { display: flex; } .str-chat__channel-list { width: 30%; } .str-chat__channel { width: 100%; } .str-chat__thread { width: 45%; }
Now that the stylesheet is ready we can pass this theme class name to the theme property of the Chat component so we can see the change in styles:
1234567import { Chat } from "stream-chat-react"; // ... return ( <Chat client={client} theme='custom-theme'> {/* ... */} </Chat> );
You can read more about theming in our documentation.
Customizing Your Own UI Components
While all the React components in the library render with polished default markup and styling, you can also replace SDK-owned UI surfaces when you want a more branded experience.
The UI customization is done through the WithComponents component provider.
In this example we'll customize the channel list row and the message UI while keeping the rest of the app unchanged.
Note: Custom UI components receive the same props as their default counterparts. For message UIs, prefer
useMessageContext()so your component stays aligned with the current SDK data flow.
Update src/App.tsx with the following code:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173import React, { useEffect, useState } from 'react'; import type { ChannelFilters, ChannelOptions, ChannelSort, User } from 'stream-chat'; import { Chat, Channel, ChannelAvatar, ChannelHeader, ChannelList, MessageComposer, MessageList, Thread, Window, WithComponents, useCreateChatClient, useMessageContext, type ChannelListItemUIProps, } from 'stream-chat-react'; import './layout.css'; const apiKey = 'REPLACE_WITH_API_KEY'; const userId = 'REPLACE_WITH_USER_ID'; const userName = 'REPLACE_WITH_USER_NAME'; const userToken = 'REPLACE_WITH_USER_TOKEN'; const user: User = { id: userId, name: userName, image: `https://getstream.io/random_png/?name=${userName}`, }; const sort: ChannelSort = { last_message_at: -1 }; const filters: ChannelFilters = { type: 'messaging', members: { $in: [userId] }, }; const options: ChannelOptions = { limit: 10, }; const CustomChannelListItem = ({ active, channel, displayImage, displayTitle, latestMessagePreview, onSelect, setActiveChannel, }: ChannelListItemUIProps) => { const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => { if (onSelect) { onSelect(e); return; } setActiveChannel?.(channel, undefined, e); }; return ( <button aria-pressed={active} onClick={handleClick} style={{ width: '100%', padding: '12px', display: 'flex', gap: '12px', border: 'none', background: active ? '#d3f2ef' : 'transparent', textAlign: 'left', cursor: 'pointer', borderRadius: '20px', }} type='button' > <ChannelAvatar imageUrl={displayImage ?? channel.data?.image} size='xl' userName={displayTitle ?? channel.data?.name ?? 'Channel'} /> <div style={{ flex: 1 }}> <div>{displayTitle ?? channel.data?.name ?? 'Unnamed Channel'}</div> {latestMessagePreview ? ( <div style={{ fontSize: '14px', opacity: 0.75 }}>{latestMessagePreview}</div> ) : null} </div> </button> ); }; const CustomMessage = () => { const { message } = useMessageContext(); const isOwnMessage = message.user?.id === userId; return ( <div style={{ display: 'flex', justifyContent: isOwnMessage ? 'flex-end' : 'flex-start', padding: '4px 8px', }} > <div style={{ background: isOwnMessage ? '#d3f2ef' : '#ffffff', borderRadius: '20px', boxShadow: '0 8px 24px rgba(15, 23, 42, 0.08)', maxWidth: 'min(80%, 640px)', padding: '12px 16px', }} > <div style={{ color: '#0f172a', fontSize: '13px', fontWeight: 700 }}> {message.user?.name} </div> <div style={{ color: '#334155' }}>{message.text}</div> </div> </div> ); }; const App = () => { const [isReady, setIsReady] = useState(false); const client = useCreateChatClient({ apiKey, tokenOrProvider: userToken, userData: user, }); useEffect(() => { if (!client) return; const initChannel = async () => { const channel = client.channel('messaging', 'react-tutorial', { image: 'https://getstream.io/random_png/?name=react-v14', name: 'Talk about React', members: [userId], }); await channel.watch(); setIsReady(true); }; initChannel().catch((error) => { console.error('Failed to initialize tutorial channel', error); }); }, [client]); if (!client) return <div>Setting up client & connection...</div>; if (!isReady) return <div>Loading tutorial channel...</div>; return ( <WithComponents overrides={{ ChannelListItemUI: CustomChannelListItem, Message: CustomMessage, }} > <Chat client={client} theme='custom-theme'> <ChannelList filters={filters} sort={sort} options={options} /> <Channel> <Window> <ChannelHeader /> <MessageList /> <MessageComposer /> </Window> <Thread /> </Channel> </Chat> </WithComponents> ); }; export default App;
Because ChannelListItemUI replaces the entire channel row, this simplified example also replaces the default row affordances that normally come with the SDK-owned item, including the built-in action buttons and context menu.
If you want to preserve those controls, compose the default channel-list item structure back into your custom row rather than replacing it with a bare button.
Create a Custom Message Attachment Type
In this example, you’ll create a custom Attachment component that renders a different UI if the attachment is of type product.
To see this functionality in action, you’ll:
- Set up your app to automatically send a message with a custom attachment on mount.
- Make a query to fetch the created channel after the user connects.
- Send a message with the product attachment included. The custom
Attachmentcomponent will then render in theMessageList.
To keep this step focused on custom attachments, we'll render a single dedicated channel instead of combining it with ChannelList.
Update src/App.tsx with the following code:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140import { useEffect, useState } from 'react'; import type { Attachment as AttachmentType, Channel as StreamChannel, User, } from 'stream-chat'; import { Attachment, Chat, Channel, ChannelHeader, MessageComposer, MessageList, Thread, Window, WithComponents, useCreateChatClient, type AttachmentProps, } from 'stream-chat-react'; import './layout.css'; const apiKey = 'REPLACE_WITH_API_KEY'; const userId = 'REPLACE_WITH_USER_ID'; const userName = 'REPLACE_WITH_USER_NAME'; const userToken = 'REPLACE_WITH_USER_TOKEN'; const user: User = { id: userId, name: userName, image: `https://getstream.io/random_png/?name=${userName}`, }; const attachments: AttachmentType[] = [ { image: 'https://images-na.ssl-images-amazon.com/images/I/71k0cry-ceL._SL1500_.jpg', name: 'iPhone', type: 'product', url: 'https://goo.gl/ppFmcR', }, ]; const isProductAttachment = ( attachment: AttachmentProps['attachments'] extends Array<infer T> ? T : never, ): attachment is AttachmentType => 'type' in attachment && attachment.type === 'product'; const CustomAttachment = (props: AttachmentProps) => { const { attachments } = props; const [attachment] = attachments || []; if (attachment && isProductAttachment(attachment)) { return ( <div style={{ background: '#ffffff', borderRadius: '24px', boxShadow: '0 10px 30px rgba(15, 23, 42, 0.08)', padding: '12px', }} > <div style={{ color: '#0f172a', fontSize: '12px', fontWeight: 700 }}> Product recommendation </div> <a href={attachment.url} rel='noreferrer' target='_blank'> <img alt='custom-attachment' height='120' src={attachment.image} style={{ borderRadius: '18px', marginTop: '8px', objectFit: 'cover' }} /> <div style={{ color: '#334155', marginTop: '8px' }}>{attachment.name}</div> </a> </div> ); } return <Attachment {...props} />; }; const App = () => { const [channel, setChannel] = useState<StreamChannel>(); const client = useCreateChatClient({ apiKey, tokenOrProvider: userToken, userData: user, }); useEffect(() => { if (!client) return; const initChannel = async () => { const channel = client.channel('messaging', 'react-tutorial-products', { image: 'https://getstream.io/random_png/?name=products', name: 'Product recommendations', members: [userId], }); await channel.watch(); const hasProductMessage = channel.state.messages.some((message) => message.attachments?.some( (attachment) => 'type' in attachment && attachment.type === 'product', ), ); if (!hasProductMessage) { await channel.sendMessage({ text: 'Your selected product is out of stock, would you like to select one of these alternatives?', attachments, }); } setChannel(channel); }; initChannel().catch((error) => { console.error('Failed to initialize attachments', error); }); }, [client]); if (!client) return <div>Setting up client & connection...</div>; if (!channel) return <div>Loading tutorial channel...</div>; return ( <WithComponents overrides={{ Attachment: CustomAttachment }}> <Chat client={client} theme='custom-theme'> <Channel channel={channel}> <Window> <ChannelHeader /> <MessageList /> <MessageComposer /> </Window> <Thread /> </Channel> </Chat> </WithComponents> ); }; export default App;
Again, we need to declare custom attachment properties name, image, url in our d.ts file for proper type-checking:
12345678910111213import { DefaultAttachmentData, DefaultChannelData } from 'stream-chat-react'; declare module 'stream-chat' { interface CustomAttachmentData extends DefaultAttachmentData { image?: string; name?: string; url?: string; } interface CustomChannelData extends DefaultChannelData { image?: string; } }
For more information read the React SDK Overview documentation or if you want to build a more complex chat application, review Stream’s API documentation.
Enable EmojiPicker and Emoji Autocomplete
No chat experience is complete without emojis - we've made it easy to extend your MessageComposer with the SDK EmojiPicker component and emoji autocomplete (through the use of SearchIndex).
On
npm,@emoji-mart/reactdeclares a peer dependency on React 18 or earlier, which conflicts with React ^19. To install it on React ^19 you need to do both of the following:
Add the following
overridesblock to yourpackage.jsonso npm resolves@emoji-mart/react's React peer to your app's React version:json123456"overrides": { "@emoji-mart/react": { "react": "$react", "react-dom": "$react-dom" } }Pass
--legacy-peer-depson the install command below. npm validates the newly-added package's peer range before consultingoverrides, so the override alone is not enough on the first install.No changes needed for the
pnpmoryarnpackage manager.
For this part we'll need emoji-mart related packages on top of which our SDK components are built (make sure versions of these packages fit within our peer dependency requirements), to install these, run:
1npm i emoji-mart @emoji-mart/react @emoji-mart/data --legacy-peer-deps
1yarn add emoji-mart @emoji-mart/react @emoji-mart/data
And now the actual code:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788import { useEffect, useState } from 'react'; import type { ChannelFilters, ChannelSort, User } from 'stream-chat'; import { Chat, Channel, ChannelHeader, ChannelList, MessageComposer, MessageList, Thread, Window, WithComponents, useCreateChatClient, } from 'stream-chat-react'; import { EmojiPicker } from 'stream-chat-react/emojis'; import { init, SearchIndex } from 'emoji-mart'; import data from '@emoji-mart/data'; import './layout.css'; const apiKey = 'REPLACE_WITH_API_KEY'; const userId = 'REPLACE_WITH_USER_ID'; const userName = 'REPLACE_WITH_USER_NAME'; const userToken = 'REPLACE_WITH_USER_TOKEN'; const user: User = { id: userId, name: userName, image: `https://getstream.io/random_png/?name=${userName}`, }; const sort: ChannelSort = { last_message_at: -1 }; const filters: ChannelFilters = { type: 'messaging', members: { $in: [userId] }, }; init({ data }); const App = () => { const [isReady, setIsReady] = useState(false); const client = useCreateChatClient({ apiKey, tokenOrProvider: userToken, userData: user, }); useEffect(() => { if (!client) return; const initChannel = async () => { const channel = client.channel('messaging', 'react-tutorial', { image: 'https://getstream.io/random_png/?name=react-v14', name: 'Talk about React', members: [userId], }); await channel.watch(); setIsReady(true); }; initChannel().catch((error) => { console.error('Failed to initialize tutorial channel', error); }); }, [client]); if (!client) return <div>Setting up client & connection...</div>; if (!isReady) return <div>Loading tutorial channel...</div>; return ( <Chat client={client}> <WithComponents overrides={{ EmojiPicker }}> <ChannelList filters={filters} sort={sort} /> <Channel> <Window> <ChannelHeader /> <MessageList /> <MessageComposer emojiSearchIndex={SearchIndex} /> </Window> <Thread /> </Channel> </WithComponents> </Chat> ); }; export default App;
Create a Livestream Style Chat App
For the next example, you’ll customize the original code snippet to work well for a livestream style chat application. In livestream apps, the user interface tends to be more compact and message seen/read states can get noisy as volume increases.
For this reason, we've changed the channel type and switched the message list to use the VirtualizedMessageList component, which handles list virtualization out-of-the-box and manages memory build-up.
Update src/App.tsx with the following code to see a simple livestream example:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768import { useEffect, useState } from 'react'; import type { Channel as StreamChannel, User } from 'stream-chat'; import { Channel, ChannelHeader, Chat, MessageComposer, VirtualizedMessageList, Window, useCreateChatClient, } from 'stream-chat-react'; import './layout.css'; const apiKey = 'REPLACE_WITH_API_KEY'; const userId = 'REPLACE_WITH_USER_ID'; const userName = 'REPLACE_WITH_USER_NAME'; const userToken = 'REPLACE_WITH_USER_TOKEN'; const user: User = { id: userId, name: userName, image: `https://getstream.io/random_png/?name=${userName}`, }; const App = () => { const [channel, setChannel] = useState<StreamChannel>(); const chatClient = useCreateChatClient({ apiKey, tokenOrProvider: userToken, userData: user, }); useEffect(() => { if (!chatClient) return; const initChannel = async () => { const spaceChannel = chatClient.channel('livestream', 'spacex', { image: 'https://goo.gl/Zefkbx', name: 'SpaceX launch discussion', }); await spaceChannel.watch(); setChannel(spaceChannel); }; initChannel().catch((error) => { console.error('Failed to initialize livestream channel', error); }); }, [chatClient]); if (!chatClient) return <div>Setting up client & connection...</div>; if (!channel) return <div>Loading tutorial channel...</div>; return ( <Chat client={chatClient} theme='str-chat__theme-dark'> <Channel channel={channel}> <Window> <ChannelHeader /> <VirtualizedMessageList /> <MessageComposer focus /> </Window> </Channel> </Chat> ); }; export default App;
There are a few important differences compared to the first example:
- You're using the
livestreamchannel type, which disables typing events and seen/read states. - You set the theme to
str-chat__theme-dark, which enables dark mode for the livestream channel type. - You're using the
VirtualizedMessageListcomponent, which supports the high message volume unique to livestream events. - You're still using the same
MessageComposersurface, so commands, uploads, and the redesigned composer UX continue to work in a more compact layout.
Final Thoughts
In this chat app tutorial we built a fully functioning React messaging app with our React SDK component library. We started with the polished defaults, then customized them with theme tokens and scoped component overrides.
Both the chat SDK for React and the API have more features available to support more advanced use-cases such as push notifications, content moderation, rich messages, and more. Please check out our React Native tutorial, too. If you want some inspiration for your app, download our free chat interface UI kit.
As a next step, check out the Stream React examples source code for the chat demos on Stream’s website. Here, you can view more detailed integrations and complex use-cases involving Stream's React components.
