Search
Search finds channels, people and messages from the search bar above the channel list. This page covers what users get, how the search UI connects to the state layer, and how to change what it searches and shows. How the sources query and paginate is on Search sources.
What users get
The user types "des" in the search bar. The channel list makes way for the results:
- Buttons for channels, users and messages choose what to search. One is selected at a time, channels first.
- The results load as they type, and more load as they scroll. Below the last one, "All results loaded" says there are no more.
- If they clear the text, the results area says "Start typing to search" until they type again.
What a result does when they click it:
| Result | What happens |
|---|---|
| Channel | The channel opens, is watched, and appears in the channel lists it belongs to. |
| Message | The message's channel opens and jumps to the message, which is highlighted. |
| User | A direct message channel with that user opens. The server creates it if it doesn't exist yet. |
The clear button empties the search text. Cancel, or the Escape key, closes search, and the channel lists come back.
How it works across the layers
State: the search controller
Search is a SearchController from stream-chat, with one source per kind of result. The controller holds the search text and whether search is open. Each source holds its own results, page by page. The results are a snapshot: unlike a channel list, they don't gain or lose items as events arrive. Search sources explains the controller, the built-in sources and how each one queries.
Chat creates a controller with three sources, ChannelSearchSource, UserSearchSource and MessageSearchSource, and releases it when it unmounts. To use your own controller, pass it to Chat as searchController. Components read the controller with useChatContext().
UI: the search components
Search renders the search UI. It reads the controller from Chat, so it works anywhere inside Chat. ChannelNavigation renders it above the channel lists, and renders the lists only while search is closed.
Search is built from parts, each one a key you can override:
SearchBar: the input, the clear button and the Cancel button. Typing opens search and passes the text to the controller.SearchResults: shown while search is open. It renders:SearchResultsHeader, the buttons that choose the source;SearchResultsPresearch, while the search text is empty;SearchSourceResultsfor each active source:SearchSourceResultsHeader, which renders nothing by default, thenSearchSourceResultListwith the results, orSearchSourceResultsEmptywhen the source found nothing.
SearchSourceResultListrenders the results in an infinite scroll, withSearchSourceResultListFooterand itsSearchSourceResultsLoadingIndicatorat the end.
Search provides SearchContext: the controller and the Search props. Inside SearchSourceResults, useSearchSourceResultsContext() gives the source that part renders.
Opening a result
The result items open what was selected through useWorkspaceNavigation(), so the layout decides where it lands. Without a layout, nothing opens; see Navigation.
Opening also changes state:
- A channel result watches the channel, because the channel search doesn't, and adds it to the lists that match it.
- A message result loads the message's channel if it isn't loaded, and jumps to the message with
channel.messagePaginator.jumpToMessage(). - A user result gets the direct message channel with
client.channel(), watches it, and then adds it to the lists.
When the channel can't be loaded, the result publishes a "Failed to load the channel" notification. Moving between channels, threads and messages shows where each one opens.
Turning it on or off
Search is on wherever ChannelNavigation or Search renders. To remove it from ChannelNavigation, set a component that renders nothing on the Search key:
import type { PropsWithChildren } from "react";
import { WithComponents } from "stream-chat-react";
const NoSearch = () => null;
const overrides = { Search: NoSearch };
export const WithoutSearch = ({ children }: PropsWithChildren) => (
<WithComponents overrides={overrides}>{children}</WithComponents>
);The search UI checks no capabilities. The server decides what each query returns, and message search covers only the channel types whose search setting is on; see Channel types.
Customizing
Changing the search bar's behaviour
Search takes a few props: placeholder, disabled, exitSearchOnInputBlur to close search when focus leaves it, inputProps for the <input>, and directMessagingChannelType, the channel type a user result opens (messaging by default). To set them on the Search that ChannelNavigation renders, override the Search key with a component that renders Search:
import type { PropsWithChildren } from "react";
import { Search, WithComponents } from "stream-chat-react";
const SidebarSearch = () => (
<Search
directMessagingChannelType="team"
exitSearchOnInputBlur
placeholder="Search channels, people and messages"
/>
);
const overrides = { Search: SidebarSearch };
export const WithSidebarSearch = ({ children }: PropsWithChildren) => (
<WithComponents overrides={overrides}>{children}</WithComponents>
);Choosing what to search
Pass your own controller to Chat. This one searches channels and users, but not messages:
import { useEffect, useMemo } from "react";
import type { PropsWithChildren } from "react";
import {
ChannelSearchSource,
SearchController,
UserSearchSource,
} from "stream-chat";
import type { StreamChat } from "stream-chat";
import { Chat } from "stream-chat-react";
export const ChatWithoutMessageSearch = ({
children,
client,
}: PropsWithChildren<{ client: StreamChat }>) => {
const searchController = useMemo(
() =>
new SearchController({
client,
sources: [
new ChannelSearchSource(client),
new UserSearchSource(client),
],
}),
[client],
);
// Chat starts and releases only the controller it creates. This one is the app's.
useEffect(() => {
searchController.registerSubscriptions();
return () => searchController.dispose();
}, [searchController]);
return (
<Chat client={client} searchController={searchController}>
{children}
</Chat>
);
};Create the controller once per client, as useMemo does here. A new controller on every render starts every search from scratch. To change what a source queries, such as its filters and sort, configure the source; see Search sources.
Changing how results look
SearchSourceResultList takes SearchResultItems, a map from a source's type to the component that renders one of its results. Each component gets the result as item. Start from DefaultSearchResultItems and replace one entry. This one keeps the default user result, with its click behaviour, and adds a label for admins:
import type { PropsWithChildren } from "react";
import {
DefaultSearchResultItems,
SearchSourceResultList,
UserSearchResultItem,
WithComponents,
} from "stream-chat-react";
import type { UserSearchResultItemProps } from "stream-chat-react";
const UserResultWithRole = (props: UserSearchResultItemProps) => (
<div className="user-result">
<UserSearchResultItem {...props} />
{props.item.role === "admin" && (
<span className="user-result__role">Admin</span>
)}
</div>
);
const ResultList = () => (
<SearchSourceResultList
SearchResultItems={{
...DefaultSearchResultItems,
users: UserResultWithRole,
}}
/>
);
const overrides = { SearchSourceResultList: ResultList };
export const WithCustomResults = ({ children }: PropsWithChildren) => (
<WithComponents overrides={overrides}>{children}</WithComponents>
);ChannelSearchResultItem, MessageSearchResultItem and UserSearchResultItem also take onSelect, which runs instead of the default when the result is clicked.
Reading search state in your own components
The controller and each source keep their state in stores. Inside Search, read them with useSearchContext() and useSearchSourceResultsContext(), and subscribe with useStateStore. This footer replaces SearchSourceResultListFooter and counts the loaded results:
import type { SearchSourceState } from "stream-chat";
import {
useSearchSourceResultsContext,
useStateStore,
} from "stream-chat-react";
const footerSelector = ({ hasNext, isLoading, items }: SearchSourceState) => ({
count: items?.length ?? 0,
hasNext,
isLoading,
});
export const ResultCountFooter = () => {
const { searchSource } = useSearchSourceResultsContext();
const { count, hasNext, isLoading } = useStateStore(
searchSource.state,
footerSelector,
);
if (isLoading) return <div className="result-footer">Loading…</div>;
if (hasNext) return null;
return <div className="result-footer">{count} results</div>;
};Register it on the SearchSourceResultListFooter key. Reading state in components explains the selector rules.
Searching your own data
A source of your own searches anything your app has, such as projects or documents. Write it as Adding your own source describes, add it to the controller, and map its type to a result component:
import { useEffect, useMemo } from "react";
import type { PropsWithChildren } from "react";
import {
ChannelSearchSource,
MessageSearchSource,
SearchController,
UserSearchSource,
} from "stream-chat";
import type { StreamChat } from "stream-chat";
import {
Chat,
DefaultSearchResultItems,
SearchSourceResultList,
WithComponents,
} from "stream-chat-react";
// A source with `type = "projects"`, built as the Search sources page shows.
import { ProjectSearchSource } from "./ProjectSearchSource";
type Project = { id: string; name: string };
const ProjectResult = ({ item }: { item: Project }) => (
<button className="project-result" type="button">
{item.name}
</button>
);
const ResultList = () => (
<SearchSourceResultList
SearchResultItems={{ ...DefaultSearchResultItems, projects: ProjectResult }}
/>
);
const overrides = { SearchSourceResultList: ResultList };
export const ChatWithProjectSearch = ({
children,
client,
}: PropsWithChildren<{ client: StreamChat }>) => {
const searchController = useMemo(
() =>
new SearchController({
client,
sources: [
new ChannelSearchSource(client),
new UserSearchSource(client),
new MessageSearchSource(client),
new ProjectSearchSource(),
],
}),
[client],
);
useEffect(() => {
searchController.registerSubscriptions();
return () => searchController.dispose();
}, [searchController]);
return (
<Chat client={client} searchController={searchController}>
<WithComponents overrides={overrides}>{children}</WithComponents>
</Chat>
);
};The new source gets a button next to channels, users and messages. Its label is the translation of search-results-header-filter-source-button-label--projects, so add that key to your translations; Localization explains how.
The Search customization and Search menu recipes go further.
Reference
- Search components: the
Searchprops and the override keys. - ChatContext:
searchController. - Chat: the
searchControllerprop. - Search sources: the controller, the sources and their state.