Indicators
The SDK's loading, empty-state, pagination and typing indicators. This page lists where each one renders, how to replace it, and its props. The Components pages show them in context: Channel list UI, Message list UI, Thread UI and Channel UI.
Best practices
- Replace an indicator through its override key, with
WithComponents. The lists don't take indicators as props. - The channel list has keys of its own, such as
EmptyListIndicator. AnEmptyStateIndicatoroverride doesn't reach it. - Keep a loading or empty state the size of what it stands in for, so the layout doesn't jump when the data arrives.
- Treat
TypingIndicator, at the bottom of the message list, andTypingIndicatorHeader, in the header, as two separate parts. - To change every spinner at once, replace the
IconLoadingicon:LoadingIndicatorrenders it.
Available components
Every component here imports from stream-chat-react.
| Component | What it shows | Where the SDK renders it | Override key |
|---|---|---|---|
EmptyStateIndicator |
A message for an empty list | MessageList and VirtualizedMessageList with no messages. The channel list's default empty state. |
EmptyStateIndicator, for the message lists |
LoadingChannels |
Five placeholder rows | The channel list and the thread list, while they load their first page | FirstPageLoadingIndicator, for the channel list |
LoadingChannel |
A placeholder header, three messages and a composer | Nowhere. Render it yourself while a channel loads. | None |
LoadingErrorIndicator |
"Error:" and the error's message | Nowhere | None |
LoadingIndicator |
A spinner | The message lists while they load a page, the thread list's next page, upload previews, and other spinners | LoadingIndicator, for the parts listed below |
LoadMoreButton |
A "Load more" button, with a spinner while loading | Only inside LoadMorePaginator |
None |
LoadMorePaginator |
Its children and a LoadMoreButton |
Nowhere. The channel list loads its next page on scroll. | None |
TypingIndicator |
The avatars of the people typing and animated dots | The bottom of MessageList, and of VirtualizedMessageList when the key is set |
TypingIndicator |
TypingIndicatorHeader |
"Cleo is typing" and animated dots | The subtitle of ChannelHeader and ThreadHeader |
None |
UI customization
EmptyStateIndicator
The message lists render the EmptyStateIndicator override with listType="message" in a channel and listType="thread" in a thread. The default shows "Send a message to start the conversation", or messageText when passed, and nothing in a thread.
The other lists have their own keys:
- the channel list renders
EmptyListIndicator, which receives no props and defaults to<EmptyStateIndicator listType="channel" />, "No conversations yet"; - the thread list renders
ThreadListEmptyPlaceholder.
LoadingErrorIndicator
LoadingErrorIndicator renders "Error: " and error.message, and nothing without an error. The SDK doesn't render it, and it has no override key. Render it yourself where a load of your own can fail. A failed channel list query is reported as a notification instead: see Channel list UI.
LoadingIndicator
These parts render the LoadingIndicator override: VirtualizedMessageList while it loads a page of messages, MessageList while it loads an older page (its loadingMore prop changes when), the thread list's default ThreadListLoadingIndicator, an attachment preview while the server confirms its upload, and a video attachment while its player loads. The SDK passes it no props that change how it looks.
Other spinners render LoadingIndicator directly, not through the key: the channel list's next page, LoadMoreButton, the unseen threads banner, poll answer and vote lists, the image gallery, and the lists in channel details. All of them, through the key or not, render the IconLoading icon, so replacing that icon changes every spinner. Icons shows how.
LoadMorePaginator and LoadMoreButton
LoadMorePaginator renders its children and, while hasNextPage is true, a LoadMoreButton that calls loadNextPage. Use them for a list of your own. ChannelList has no paginator prop: it loads the next page when it's scrolled to the bottom, and shows LoadingNextPageIndicator meanwhile.
TypingIndicator
MessageList passes the TypingIndicator override isMessageListScrolledToBottom and scrollToBottom. It shows only while the list is scrolled to the latest message, and scrolls the list down when it appears. VirtualizedMessageList has no default: it renders a typing indicator only when the TypingIndicator key is set.
It reads the typing events from the composer that useMessageComposerController() returns, leaves out the current user, and renders nothing when typing events are off for the channel. Typing events reach the channel's composer only, so inside a Thread it shows nothing. Typing indicators covers the feature end to end.
TypingIndicatorHeader
TypingIndicatorHeader replaces the subtitle of ChannelHeader and ThreadHeader while someone is typing. It has no override key: a custom header renders it itself. Without threadList, it shows typing in the channel. With threadList, it looks for typing in the thread of the Thread around it, but like TypingIndicator it reads the thread's composer there, which typing events don't reach, so it shows nothing in ThreadHeader.
EmptyStateIndicatorProps
| Prop | Type | Default | Description |
|---|---|---|---|
listType |
"channel" | "message" | "thread" |
The list it's shown in. With no listType, the default shows "No items exist". |
|
messageText |
string |
For listType="message", the text to show instead of the default. It's passed through translation. |
LoadingErrorIndicatorProps
| Prop | Type | Default | Description |
|---|---|---|---|
error |
Error | null |
The error to show. Without one, the indicator renders nothing. |
LoadingIndicatorProps
LoadingIndicatorProps are the props of an SVG element, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
decorative |
boolean |
true |
Hides the spinner from assistive technology and keeps it out of the tab order. |
It passes them to the IconLoading icon, with the class str-chat__loading-indicator in place of any className you pass.
LoadMoreButtonProps
| Prop | Type | Default | Description |
|---|---|---|---|
onClick |
React.MouseEventHandler<HTMLButtonElement> |
required | Loads the next page. |
isLoading |
boolean |
Disables the button and shows a spinner instead of its label. | |
children |
ReactNode |
"Load more" |
The label. |
LoadMorePaginatorProps
| Prop | Type | Default | Description |
|---|---|---|---|
loadNextPage |
() => void |
required | Called by the button. |
hasNextPage |
boolean |
Shows the button while true. |
|
isLoading |
boolean |
Passed to the button. | |
LoadMoreButton |
React.ComponentType<LoadMoreButtonProps> |
LoadMoreButton |
The button to render. |
reverse |
boolean |
Puts the button before the children instead of after them. | |
children |
ReactNode |
The list. |
LoadMorePaginatorProps also accepts hasPreviousPage, loadPreviousPage, LoadingIndicator and threshold, from the shared PaginatorProps type, but LoadMorePaginator doesn't read them.
TypingIndicatorProps
| Prop | Type | Default | Description |
|---|---|---|---|
isMessageListScrolledToBottom |
boolean |
true |
When false, the indicator renders nothing. |
scrollToBottom |
() => void |
Called when the indicator appears while the list is scrolled to the bottom. |
Whether it shows typing in the channel or in a thread depends on whether it renders inside a Thread, not on a prop.
TypingIndicatorHeaderProps
| Prop | Type | Default | Description |
|---|---|---|---|
threadList |
boolean |
false |
Looks for typing in the current thread instead of in the channel. Inside a Thread, it finds none: see above. |
Examples
Replace the message list's empty state
import { Channel, MessageList, WithComponents } from "stream-chat-react";
import type { EmptyStateIndicatorProps } from "stream-chat-react";
const EmptyChannel = ({ listType }: EmptyStateIndicatorProps) =>
listType === "message" ? (
<p className="app-empty-channel">No messages yet. Say hi!</p>
) : null;
export const App = () => (
<WithComponents overrides={{ EmptyStateIndicator: EmptyChannel }}>
<Channel channel={channel}>
<MessageList />
</Channel>
</WithComponents>
);Returning null for "thread" keeps a thread's list empty, as the default does.
Replace the channel list's empty state
import { ChannelList, WithComponents } from "stream-chat-react";
const NoChannels = () => (
<p className="app-no-channels">Join a channel to start.</p>
);
export const Channels = () => (
<WithComponents overrides={{ EmptyListIndicator: NoChannels }}>
<ChannelList paginator={paginator} />
</WithComponents>
);Show typing in VirtualizedMessageList
import {
Channel,
TypingIndicator,
VirtualizedMessageList,
WithComponents,
} from "stream-chat-react";
export const App = () => (
<WithComponents overrides={{ TypingIndicator }}>
<Channel channel={channel}>
<VirtualizedMessageList />
</Channel>
</WithComponents>
);