Search components
Search renders the search bar and, while a search is open, its results: channels, users and messages. It reads the SearchController that Chat provides, so it works anywhere inside Chat. ChannelNavigation renders it above the channel lists. Search explains the feature, and Search sources the controller and its sources.
Best practices
- Change what is searched on the controller and its sources, not in the components. Pass your own controller to
ChatassearchController. - Create your own controller once per client, for example with
useMemo. A new controller on every render starts every search from scratch. - Replace one part of the search UI with its override key rather than the whole
Search. - To set
Searchprops on theSearchthatChannelNavigationrenders, register a component that rendersSearchwith them under theSearchkey.
Basic usage
import type { PropsWithChildren } from "react";
import { Search, WithComponents } from "stream-chat-react";
const SidebarSearch = () => (
<Search exitSearchOnInputBlur placeholder="Search chats and people" />
);
const overrides = { Search: SidebarSearch };
export const WithSidebarSearch = ({ children }: PropsWithChildren) => (
<WithComponents overrides={overrides}>{children}</WithComponents>
);Rendered around ChannelNavigation, this replaces the search bar above the lists. Outside ChannelNavigation, render <Search /> wherever the layout needs it.
Search props:
| Prop | Type | Default | Description |
|---|---|---|---|
directMessagingChannelType |
string |
"messaging" |
The channel type of the direct message channel a user result opens. |
disabled |
boolean |
false |
Disables the input. |
exitSearchOnInputBlur |
boolean |
false |
Closes search when focus leaves the search bar and its results. |
inputProps |
React.ComponentPropsWithoutRef<"input"> |
none | Props for the <input>, such as name or autoComplete. The input's own value, onChange, onBlur, type, disabled, placeholder and aria-label win; className is added to the default. |
placeholder |
string |
"Search" | The placeholder of the input. |
Typing opens search and passes the text to the controller. The clear button empties the text. Cancel, or the Escape key, closes search.
Anatomy
Search provides SearchContext and renders SearchBar and SearchResults. Each part is read from an override key:
| Override key | Renders | Gets |
|---|---|---|
SearchBar |
The input, the clear button, and Cancel while search is open | nothing |
SearchResults |
Nothing while search is closed. Otherwise the header, then the presearch state or one SearchSourceResults per active source |
nothing |
SearchResultsHeader |
The buttons that choose the source. Nothing when the controller has fewer than two sources | nothing |
SearchResultsPresearch |
"Start typing to search", while the text is empty | activeSources |
SearchSourceResults |
One source's results, inside SearchSourceResultsContext. Nothing before the source's first search |
searchSource |
SearchSourceResultsHeader |
Nothing by default. Renders above a source's results | nothing |
SearchSourceResultList |
The results in an infinite scroll, then SearchSourceResultListFooter |
Below |
SearchSourceResultListFooter |
SearchSourceResultsLoadingIndicator while loading, "All results loaded" when there are no more |
nothing |
SearchSourceResultsLoadingIndicator |
"Searching for" and the source's type, such as "Searching for users..." | nothing |
SearchSourceResultsEmpty |
"No results found", when a source found nothing | nothing |
UI customization
Register replacements with WithComponents. The default of every key above except SearchSourceResultsHeader, which renders nothing, is exported under the same name, so a replacement can render the default with something added:
import type { PropsWithChildren } from "react";
import { SearchSourceResultsEmpty, WithComponents } from "stream-chat-react";
const EmptyWithHint = () => (
<div className="search-empty">
<SearchSourceResultsEmpty />
<p>Try a shorter word, or search messages instead.</p>
</div>
);
const overrides = { SearchSourceResultsEmpty: EmptyWithHint };
export const WithSearchHint = ({ children }: PropsWithChildren) => (
<WithComponents overrides={overrides}>{children}</WithComponents>
);A part reads the controller with useSearchContext(), and the source it renders with useSearchSourceResultsContext(). Reading search state in your own components has an example. To remove search from ChannelNavigation, register a Search that renders nothing.
Custom result items
SearchSourceResultList props:
| Prop | Type | Default | Description |
|---|---|---|---|
SearchResultItems |
Record<string, ComponentType<{ item: any }>> |
DefaultSearchResultItems |
The component for one result, by the source's type. A source with no entry renders no results. |
loadMoreDebounceMs |
number |
100 |
How long the list waits after the user stops scrolling before it checks whether to load more. |
loadMoreThresholdPx |
number |
80 |
How close to the end, in pixels, scrolling has to come to load more. |
DefaultSearchResultItems maps the built-in sources to their result components:
| Source type | Component | item |
Click |
|---|---|---|---|
channels |
ChannelSearchResultItem |
Channel |
Opens the channel and adds it to the lists it belongs to |
messages |
MessageSearchResultItem |
MessageResponse |
Opens the message's channel and jumps to the message |
users |
UserSearchResultItem |
UserResponse |
Opens the direct message channel with the user |
Each component also takes onSelect: (event: React.MouseEvent) => void, which runs instead of the default click. To replace one, start from DefaultSearchResultItems:
import type { PropsWithChildren } from "react";
import type { UserResponse } from "stream-chat";
import {
DefaultSearchResultItems,
SearchSourceResultList,
WithComponents,
} from "stream-chat-react";
const InviteUserResult = ({ item }: { item: UserResponse }) => (
<button className="invite-result" type="button">
Invite {item.name ?? item.id}
</button>
);
const ResultList = () => (
<SearchSourceResultList
SearchResultItems={{ ...DefaultSearchResultItems, users: InviteUserResult }}
/>
);
const overrides = { SearchSourceResultList: ResultList };
export const WithInviteResults = ({ children }: PropsWithChildren) => (
<WithComponents overrides={overrides}>{children}</WithComponents>
);A source of your own needs an entry under its type; see Searching your own data.
Data management
The data comes from the controller. Chat creates one with ChannelSearchSource, UserSearchSource and MessageSearchSource when the app doesn't pass searchController. To change what is searched, pass your own; Choosing what to search shows how, and Search sources explains the sources, their filters and their state.
The two search contexts hold:
| Context | Hook | Values |
|---|---|---|
SearchContext |
useSearchContext() |
searchController; containerRef and filterButtonsContainerRef; and the Search props directMessagingChannelType, disabled, exitSearchOnInputBlur, inputProps and placeholder |
SearchSourceResultsContext |
useSearchSourceResultsContext() |
searchSource, the source whose results are rendered |
Both hooks throw outside their component. Loading data hooks has their details.
Typing the context of MessageSearchSource
MessageSearchSource builds three filters, each with a filter builder of its own:
messageSearchChannelFilterBuilder: which channels to search in;messageSearchFilterBuilder: which messages to return;channelQueryFilterBuilder: which channels of the results to load.
Each builder can carry a context of your own, and the source's type parameter names them, one key per builder: messageSearchChannelContext, messageSearchContext and channelQueryContext. The third constructor argument configures the builders under messageSearchChannel, messageSearch and channelQuery. This source can limit the results to pinned messages:
import { MessageSearchSource } from "stream-chat";
const messageSource = new MessageSearchSource<{
messageSearchContext: { onlyPinned: boolean };
}>(client, undefined, {
messageSearch: {
initialContext: { onlyPinned: false },
initialFilterConfig: {
pinned: {
enabled: true,
generate: ({ onlyPinned }) =>
onlyPinned ? { pinned: { $eq: true } } : null,
},
},
},
});
// later, from a toggle in the results header
messageSource.messageSearchFilterBuilder.updateContext({ onlyPinned: true });Each builder's context also holds what the source sets itself: searchQuery for the first two, and cids, the channels to load, for the third. The source keeps its built-in generators for the message text and the channels to load, and adds yours. The new context applies from the next query.