import { Chat, ChannelList, Search, WithComponents } from "stream-chat-react";
const SidebarSearch = () => (
<Search
exitSearchOnInputBlur
placeholder="Search channels, messages, and users..."
/>
);
const App = () => (
<Chat client={client}>
<WithComponents overrides={{ Search: SidebarSearch }}>
<ChannelList
filters={filters}
options={options}
showChannelSearch
sort={sort}
/>
</WithComponents>
</Chat>
);Advanced Search
Search covers the common sidebar-search use case, but the underlying SearchController is also customizable when you need to control sources, filters, ranking, or result rendering.
Best Practices
- Keep advanced search logic in a memoized
SearchController. - Use custom sources for data changes and
WithComponentsfor UI changes. - Reuse
DefaultSearchResultItemswhen you only want to replace one source renderer. - Prefer
FilterBuilderor source filters over ad hoc query logic in React components. - Keep advanced search close to
Chatso all search consumers share the same controller.
Basic Usage
To customize the default search surface used by ChannelList, override Search from the main package:
Search Data Management
Search state is managed by the reactive SearchController class from stream-chat. It uses search sources for data retrieval and pagination. The default sources are:
ChannelSearchSourcefor channelsUserSearchSourcefor usersMessageSearchSourcefor messages
Customizing Search Data Retrieval
You can customize the built-in sources or add your own source for a custom entity. The same pattern applies whether you are introducing a brand-new source type or replacing one of the defaults.
Each custom source needs:
- a unique
type - a
query()implementation - an optional
filterQueryResults()implementation
The general pattern is:
- create a source class that extends
BaseSearchSource<T> - return
{ items }fromquery() - register the source in
SearchController - provide a matching result-item component in
SearchSourceResultList
Example: Build A Custom Search Source
The example below demonstrates how to add a new custom search source. This is the most common pattern when your search needs to include an app-specific entity that Stream Chat does not query by default.
The important pieces are:
- the source class extends
BaseSearchSource<T> - the class declares a stable
type query()returns the page of items for the current search stringfilterQueryResults()can optionally reshape that page before the UI sees itSearchSourceResultListreceives aSearchResultItemsmap so the UI knows how to render the new source type
import { useMemo } from "react";
import {
BaseSearchSource,
ChannelSearchSource,
MessageSearchSource,
SearchController,
SearchSourceOptions,
UserSearchSource,
} from "stream-chat";
import {
Chat,
DefaultSearchResultItems,
Search,
SearchSourceResultList,
WithComponents,
} from "stream-chat-react";
type Project = { id: string; name: string };
class ProjectSearchSource extends BaseSearchSource<Project> {
// the search source type is used as the source key in SearchController
readonly type = "projects";
constructor(options?: SearchSourceOptions) {
super(options);
}
// query() always receives the current search string
protected async query(searchQuery: string) {
return searchQuery.length > 1
? { items: [{ id: "alpha", name: `Project ${searchQuery}` }] }
: { items: [] };
}
// filterQueryResults() is optional; use it when you want to adjust the retrieved page
protected filterQueryResults(items: Project[]) {
return items;
}
}
// this component renders one item for the custom source
const ProjectResult = ({ item }: { item: Project }) => (
<button type="button">{item.name}</button>
);
// SearchSourceResultList needs to know which component renders each source type
const SearchResultList = () => (
<SearchSourceResultList
SearchResultItems={{
...DefaultSearchResultItems,
projects: ProjectResult,
}}
/>
);
const App = () => {
const searchController = useMemo(() => {
if (!client) return;
return new SearchController({
sources: [
new ProjectSearchSource(),
new ChannelSearchSource(client),
new UserSearchSource(client),
new MessageSearchSource(client),
],
});
}, [client]);
if (!searchController) return null;
return (
<Chat client={client} searchController={searchController}>
<WithComponents
overrides={{ Search, SearchSourceResultList: SearchResultList }}
>
<ChannelList
filters={filters}
options={options}
showChannelSearch
sort={sort}
/>
</WithComponents>
</Chat>
);
};That same pattern can be used to override a default source as well. In that case, you would still create the source instance yourself and register it in SearchController, but you would use ChannelSearchSource, UserSearchSource, or MessageSearchSource instead of extending BaseSearchSource.
Example: Replace A Built-In Source
Sometimes you do not need a brand-new source type. You just need the existing channel or user source to query with different defaults. In that case, instantiate the built-in source yourself and pass it into SearchController.
import { useMemo } from "react";
import {
ChannelSearchSource,
SearchController,
UserSearchSource,
} from "stream-chat";
const useCustomSearchController = (client: StreamChat | null, userId: string) =>
useMemo(() => {
if (!client) return undefined;
const channelSearchSource = new ChannelSearchSource(client, {
filters: { members: { $in: [userId] }, type: "messaging" },
});
const userSearchSource = new UserSearchSource(client, {
filters: { id: { $ne: userId } },
});
return new SearchController({
sources: [channelSearchSource, userSearchSource],
});
}, [client, userId]);This approach keeps the SDK's built-in search-source behavior, pagination, and result types, but swaps in your own query configuration.
You can also override query parameters such as filters, sort, or searchOptions by assignment:
import { useChatContext } from "stream-chat-react";
const SearchSourceFilters = () => {
const { searchController } = useChatContext();
const usersSearchSource = searchController.getSource("users");
usersSearchSource.filters = {
...usersSearchSource.filters,
myCustomField: "some-value",
};
return null;
};Search Request Dynamic Filter Building
Default search sources include FilterBuilder instances for dynamic filter building.
The built-in sources expose these filter builders:
ChannelSearchSourcehaschannelSearchSource.filterBuilderUserSearchSourcehasuserSearchSource.filterBuilderMessageSearchSourcehas:messageSearchSource.messageSearchChannelFilterBuildermessageSearchSource.messageSearchFilterBuildermessageSearchSource.channelQueryFilterBuilder
You can define initialFilterConfig and initialContext when creating a source. The generator functions should return either:
- a partial filter object that gets merged into the final filter
nullto skip that generator
import { ChannelSearchSource } from "stream-chat";
const channelSearchSource = new ChannelSearchSource<{ archived: boolean }>(
client,
undefined,
{
initialContext: { archived: false },
initialFilterConfig: {
archived: {
enabled: true,
generator: ({ archived }) => ({ archived }),
},
memberUserName: {
enabled: true,
generator: ({ searchQuery }) =>
searchQuery
? { "member.user.name": { $autocomplete: searchQuery } }
: null,
},
},
},
);When you need more than one source with custom filter builders, create the SearchController explicitly:
import { useMemo } from "react";
import {
ChannelSearchSource,
MessageSearchSource,
SearchController,
UserSearchSource,
} from "stream-chat";
const useCustomSearchController = (client: StreamChat | null) =>
useMemo(() => {
if (!client) return undefined;
const channelSearchSource = new ChannelSearchSource<{ archived: boolean }>(
client,
undefined,
{
initialContext: { archived: false },
initialFilterConfig: {
archived: {
enabled: true,
generator: ({ archived }) => ({ archived }),
},
},
},
);
const userSearchSource = new UserSearchSource(client);
const messageSearchSource = new MessageSearchSource<{
messageSearchChannelContext: { region: string };
messageSearchContext: { onlyPinned: boolean };
channelQueryContext: { includeArchived: boolean };
}>(client);
return new SearchController({
sources: [channelSearchSource, userSearchSource, messageSearchSource],
});
}, [client]);It is also possible to enable or disable individual generators at runtime:
const searchSource = new ChannelSearchSource(client);
searchSource.filterBuilder.disableFilter("name");
searchSource.filterBuilder.enableFilter("name");And you can update the filter-builder context reactively:
const searchSource = new ChannelSearchSource<{ archived: boolean }>(client);
searchSource.filterBuilder.setContext({ archived: true });That reactive state can be consumed from your custom search UI:
import { useSearchContext, useStateStore } from "stream-chat-react";
import type {
ChannelSearchSource,
ChannelSearchSourceFilterBuilderContext,
FilterBuilderGenerators,
} from "stream-chat";
const contextSelector = ({
archived,
}: ChannelSearchSourceFilterBuilderContext<{ archived: boolean }>) => ({
archived,
});
const filterGeneratorEnablementSelector = (
generators: FilterBuilderGenerators<
ChannelFilters,
ChannelSearchSourceFilterBuilderContext<{ archived: boolean }>
>,
) => ({
archivedFilterEnabled: generators.archived.enabled,
});
export const SearchResultsHeader = () => {
const { searchController } = useSearchContext();
const channelSearchSource = searchController.getSource(
"channels",
) as ChannelSearchSource<{ archived: boolean }>;
const { archived } = useStateStore(
channelSearchSource.filterBuilder.context,
contextSelector,
);
const { archivedFilterEnabled } = useStateStore(
channelSearchSource.filterBuilder.filterConfig,
filterGeneratorEnablementSelector,
);
return (
<div className="str-chat__search-results-header">
<label>
Archived
<input
checked={archived}
type="checkbox"
onChange={() =>
channelSearchSource.filterBuilder.setContext({
archived: !archived,
})
}
/>
</label>
<label>
Enable archived filter
<input
checked={archivedFilterEnabled}
type="checkbox"
onChange={() =>
archivedFilterEnabled
? channelSearchSource.filterBuilder.disableFilter("archived")
: channelSearchSource.filterBuilder.enableFilter("archived")
}
/>
</label>
</div>
);
};Search UI Components And Their Customization
The default search UI components can be overridden through ComponentContext.
Search
The top-level component that renders SearchBar and SearchResults.
SearchBar
The input layer, clear button, and cancel button.
SearchResults
The top-level results container for one or more active search sources.
SearchResultsPresearch
Rendered when search is active but the query is still empty.
SearchResultsHeader
Rendered above the source-specific results pane.
SearchSourceResults
Rendered once per active search source. It coordinates:
SearchSourceResultsHeaderSearchSourceResultsEmptySearchSourceResultList
SearchSourceResultList
Renders the source items inside an InfiniteScrollPaginator. It can also receive a SearchResultItems map that decides how each source type should render.
SearchSourceResultListFooterrenders the footer areaSearchSourceResultsLoadingIndicatorrenders the loading state within that footer
Contexts
SearchContext
Search provides SearchContext to its child components.
Values
| Value | Description | Type |
|---|---|---|
directMessagingChannelType | The channel type to create on user-result selection. Defaults to messaging. | string |
disabled | Whether the input is disabled. | boolean |
exitSearchOnInputBlur | Whether the empty search UI should exit on input blur. | boolean |
placeholder | Placeholder text forwarded from Search. | string |
searchController | The SearchController instance from Chat. | SearchController |
You can consume the searchController reactively:
import {
SearchResultsHeader,
SearchResultsPresearch,
SearchSourceResults,
useSearchContext,
useStateStore,
} from "stream-chat-react";
import type { SearchControllerState } from "stream-chat";
const searchControllerStateSelector = (nextValue: SearchControllerState) => ({
activeSources: nextValue.sources.filter((source) => source.isActive),
isActive: nextValue.isActive,
searchQuery: nextValue.searchQuery,
});
export const SearchResults = () => {
const { searchController } = useSearchContext();
const { activeSources, isActive, searchQuery } = useStateStore(
searchController.state,
searchControllerStateSelector,
);
return isActive ? (
<div>
<SearchResultsHeader />
{!searchQuery ? (
<SearchResultsPresearch activeSources={activeSources} />
) : (
activeSources.map((source) => (
<SearchSourceResults key={source.type} searchSource={source} />
))
)}
</div>
) : null;
};SearchSourceResultsContext
SearchSourceResults provides useSearchSourceResultsContext() for the active source pane. That lets child components react to source-level state such as pagination or loading:
import {
useSearchSourceResultsContext,
useStateStore,
} from "stream-chat-react";
import type { SearchSourceState } from "stream-chat";
const searchSourceStateSelector = (value: SearchSourceState) => ({
hasMore: value.hasMore,
isLoading: value.isLoading,
});
const SearchSourceFooter = () => {
const { searchSource } = useSearchSourceResultsContext();
const { hasMore, isLoading } = useStateStore(
searchSource.state,
searchSourceStateSelector,
);
return (
<div>
{isLoading ? (
<div>Loading…</div>
) : !hasMore ? (
<div>All results loaded</div>
) : null}
</div>
);
};