import { StreamChat } from "stream-chat";
import { Chat, ChannelList, WithComponents } from "stream-chat-react";
import { Search } from "stream-chat-react/experimental";
const Component = () => (
<Chat client={client}>
<WithComponents overrides={{ Search }}>
<ChannelList
// Enable search in ChannelList
showChannelSearch={true}
// Optional: Additional search props
additionalChannelSearchProps={{
// Clear search on click outside
clearSearchOnClickOutside: true,
// Custom placeholder
placeholder: "Search channels, messages and users...",
// Custom debounce time for search
debounceMs: 500,
}}
/>
</WithComponents>
</Chat>
);
Search
The React SDK has a default search functionality handled by ChannelSearch
component. The ChannelSearch
component, however, has some limitations:
- It is not customizable in terms of what is searched respectively what API endpoints are hit to retrieve the search results. The search logic is contained in a hook that is not customizable otherwise than with prop-drilled callbacks
- The search UI components cannot be customized otherwise than overriding the whole ChannelSearch component
The experimental Search component aims to address these limitations by the following means:
- Allows for limitless customization by relying on
SearchController
class that manages the search logic - The search UI components can be customized via
ComponentContext
created with theWithComponents
component
Basic Usage
To replace the rendering of ChannelSearch
by Search
component, we need to import it from the experimental package and create a component context around the ChannelList
that will render the Search
:
Search Data Management
The search state is managed by SearchController
class and reactive. It is exported by the stream-chat
library. The class relies on so-called search sources to retrieve the data and take care of the pagination. There are three search sources available in stream-chat
library:
ChannelSearchSource
- queries channels byname
partially matching the search queryUserSearchSource
- queries users byname
orid
partially matching the search queryMessageSearchSource
- queries messages and corresponding channels by messagetext
partially matching the search query
Customizing Search Data Retrieval
We can customize the retrieval parameters of the existing search sources as well as add new search sources that would retrieve custom data entities (other than channels, users or messages).
In the example below we demonstrate how to add a new custom search source. The pattern is however applicable to overriding the default search sources. Specifically, each new search source class has to implement abstract methods query
and filterQueryResults
. Also, the class should declare type
attribute so that the SearchController
can keep the source mapping.
import { useMemo } from "react";
import {
BaseSearchSource,
ChannelSearchSource,
MessageSearchSource,
SearchController,
SearchSourceOptions,
UserSearchSource,
} from "stream-chat";
import { Chat, useCreateChatClient, WithComponents } from "stream-chat-react";
import {
DefaultSearchResultItems,
Search,
SearchSourceResultList,
} from "stream-chat-react/experimental";
// declare the type of item that is stored in the array by the search source
type X = { x: string };
// declare the custom search source
class XSearchSource extends BaseSearchSource<X> {
// search source type is necessary
readonly type = "X";
constructor(options?: SearchSourceOptions) {
super(options);
}
// the query method will always receive the searched string
protected async query(searchQuery: string) {
return searchQuery.length > 1
? { items: [{ x: "hi" }] }
: { items: [{ x: "no" }] };
}
// we can optionally manipulate the retrieved page of items
protected filterQueryResults(items: X[]): X[] {
return items;
}
}
// we need a custom component to display the search source items
const XSearchResultItem = ({ item }: { item: X }) => <div>{item.x}</div>;
// and we tell the component that renders the resulting list, what components it can use to display the items
const customSearchResultItems = {
...DefaultSearchResultItems,
X: XSearchResultItem,
};
const CustomSearchResultList = () => (
<SearchSourceResultList SearchResultItems={customSearchResultItems} />
);
const App = () => {
const chatClient = useCreateChatClient({
apiKey,
tokenOrProvider: userToken,
userData: { id: userId },
});
// create a memoized instance of SearchController
const searchController = useMemo(
() =>
chatClient
? new SearchController({
sources: [
new XSearchSource(),
new ChannelSearchSource(chatClient),
new UserSearchSource(chatClient),
new MessageSearchSource(chatClient),
],
})
: undefined,
[chatClient],
);
if (!chatClient) return <>Loading...</>;
return (
<Chat client={chatClient} searchController={searchController}>
<WithComponents
overrides={{
Search,
SearchSourceResultList: CustomSearchResultList,
}}
>
{/* ....*/}
</WithComponents>
</Chat>
);
};
The search source query parameters like filters
, sort
or searchOptions
are overridable by a simple assignment:
const { searchController } = useChatContext();
const usersSearchSource = searchController.getSource("users");
usersSearchSource.filters = {
...usersSearchSource.filters,
myCustomField: "some-value",
};
Search Request Dynamic Filter Building
It is possible to build request filters dynamically as of stream-chat@9.17.0
.
The default search sources keep FilterBuilder instances that allow for dynamic filter building at runtime.
The default search sources count with at least one filter builder:
ChannelSearchSource
has a single FilterBuilder instance, that can be accessed withchannelSearchSource.filterBuilder
UserSearchSource
has a single FilterBuilder instance, that can be accessed withuserSearchSource.filterBuilder
MessageSearchSource
has three FilterBuilder instances as three different filters has to be built. These can be accessed as:a.
messageSearchSource.messageSearchChannelFilterBuilder
- builds filters for the first argument toclient.search()
b.
messageSearchSource.messageSearchFilterBuilder
- builds filters for the second argument toclient.search()
c.
messageSearchSource.channelQueryFilterBuilder
- builds filters for the first argument toclient.queryChannels()
(retrieves the channels corresponding to the messages returned fromclient.search()
)
It is possible to define the custom filter config at the search source initiation. In that case, we need to create SearchController
instance too. Alongside initialFilterConfig
we can define initialContext
, which represents custom fields that we want to have available at the filter build time. Besides the custom context fields, every search source counts with field searchQuery
as a part of the filter builder context.
We can register filter generators via initialFilterConfig
as follows:
- register the configuration object under a key that will allow us to target that filter builder
- determine whether the filter is enabled at the beginning
- determine the generator logic
It is important to note that the generator functions should:
- return the partial filter object that is internally merged into the resulting filter or
null
which would signal the partial from the given generator should to be ignored
An example would be
// generic type determines the custom fields for the filter builder context available in the generator functions
new ChannelSearchSource<{ archived: boolean }>(
//... previous args
{
// initial context does not contain searchQuery as that is handled by the search source at the runtime
initialContext: { archived: false },
initialFilterConfig: {
memberUserName: {
// 1. register the configuration object under a key
enabled: true, // 2. determine whether the filter is enabled at the beginning
// the archived custom context property is available along with the default searchQuery
generate: (
{ archived, searchQuery }, // 3. determine the generator logic
) =>
searchQuery
? {
"member.user.name": { $autocomplete: searchQuery },
}
: null,
},
//... other filters for ChannelSearchSource
},
},
);
An example of creating a SearchController with custom filter generator configuration could look as follows:
const useCustomSearchController = (chatClient: StreamChat | null) => {
// create a memoized instance of SearchController
return useMemo(
() =>
chatClient
? new SearchController({
sources: [
new ChannelSearchSource<{ archived: boolean }>(
chatClient,
{}, // default search options are kept, but the empty object has to be provided due to positional arguments
{
initialContext: { archived: false },
initialFilterConfig: {
archived: {
enabled: true,
generate: ({ archived }) => ({
archived,
}),
},
memberUserName: {
enabled: true,
generate: ({ searchQuery }) =>
searchQuery
? {
"member.user.name": {
$autocomplete: searchQuery,
},
}
: null,
},
custom_loading_places_country_codes: {
enabled: false,
generate: ({ searchQuery }) =>
searchQuery
? {
// custom property has to be declared
custom_loading_places_country_codes: {
$in: [searchQuery],
},
}
: null,
},
},
},
),
new UserSearchSource(
chatClient,
{}, // default search options are kept, but the empty object has to be provided due to positional arguments
{
// again custom initialContext and (or) initialFilterConfig
},
),
new MessageSearchSource<{
// context definition for each filter builder
messageSearchChannelContext: { a: string };
messageSearchContext: { b: string };
channelQueryContext: { c: string };
}>(
client,
{}, // default search options are kept, but the empty object has to be provided due to positional arguments
{
messageSearchChannelFilterBuilder: {
initialContext: { a: "messageSearchChannelFilterBuilder" },
initialFilterConfig: {
custom: {
enabled: true,
generate: ({ searchQuery, a }) =>
searchQuery
? { name: { $autocomplete: searchQuery + a } }
: null,
},
},
},
messageSearchFilterBuilder: {
initialContext: { b: "messageSearchFilterBuilder" },
initialFilterConfig: {
text: {
enabled: true,
generate: ({ searchQuery, b }) =>
searchQuery ? { text: searchQuery + b } : null,
},
},
},
channelQueryFilterBuilder: {
initialContext: { c: "channelQueryFilterBuilder" },
initialFilterConfig: {
cid: {
enabled: true,
generate: ({ cids, c }) =>
cids
? { cid: { $in: cids.concat([c as string]) } }
: null,
},
},
},
},
),
],
})
: undefined,
[chatClient],
);
};
See our guide on declaring custom fields for entities like channels, messages, attachments etc.
It is possible to disable or enable back the individual filter generators:
const searchSource = new ChannelSearchSource(client);
// disables the default filter called name
searchSource.filterBuilder.disableFilter("name");
// enables the default filter called name
searchSource.filterBuilder.enableFilter("name");
It is possible to adjust the filter context
const searchSource = new ChannelSearchSource(client);
// Merges the archived key with the rest of the context. Existing key is overidden.
searchSource.filterBuilder.updateContext({ archived: true });
And finally putting it all together within a React component, we can see that searchSource.filterBuilder.context
and searchSource.filterBuilder.filterConfig
are reactive. That means we can subscribe and react to context being updated or filters being enabled / disabled:
import {
ChannelFilters,
ChannelSearchSource,
ChannelSearchSourceFilterBuilderContext,
FilterBuilderGenerators,
} from "stream-chat";
import { useSearchContext, useStateStore } from "stream-chat-react";
const contextSelector = ({
archived,
}: ChannelSearchSourceFilterBuilderContext<{ archived: boolean }>) => ({
archived,
});
const filterGeneratorEnablementSelector = (
generators: FilterBuilderGenerators<
ChannelFilters,
ChannelSearchSourceFilterBuilderContext<{
archived: boolean;
}>
>,
) => ({
custom_loading_places_country_codes:
generators.custom_loading_places_country_codes.enabled,
});
const SearchSourceFilterButton = () => {
// activates individual search sources
};
export const SearchResultsHeader = () => {
const { searchController } = useSearchContext();
const channelSearchSource = (searchController.getSource("channels") ??
{}) as ChannelSearchSource<{ archived: boolean }>;
const { archived } = useStateStore(
channelSearchSource.filterBuilder?.context,
contextSelector,
);
const { custom_loading_places_country_codes } = useStateStore(
channelSearchSource.filterBuilder?.filterConfig,
filterGeneratorEnablementSelector,
);
return (
<div
className="str-chat__search-results-header"
data-testid="search-results-header"
>
<div
className="str-chat__search-results-header__filter-source-buttons"
data-testid="filter-source-buttons"
>
{searchController.sources.map((source) => (
<SearchSourceFilterButton
key={`search-source-filter-button-${source.type}`}
source={source}
/>
))}
<label>
Archived
<input
onClick={() =>
channelSearchSource?.filterBuilder.updateContext({
archived: !archived,
})
}
type="checkbox"
value={archived.toString()}
/>
</label>
<label>
custom_loading_places_country_codes
<input
onClick={() =>
custom_loading_places_country_codes
? channelSearchSource?.filterBuilder.disableFilter(
"custom_loading_places_country_codes",
)
: channelSearchSource?.filterBuilder.enableFilter(
"custom_loading_places_country_codes",
)
}
type="checkbox"
value={custom_loading_places_country_codes.toString()}
/>
</label>
</div>
</div>
);
};
Search UI Components And Their Customization
The default search UI components can be overridden through the component context, using the default component names. There are branch components that render other components and leaf components that render the markup.
Search
The top-level component for rendering SearchBar
and SearchResults
SearchBar
A leaf component that handles the message input value
SearchResults
The top-level component for displaying search results for one or more search sources.
SearchResultsPresearch
The default component rendered by SearchResults
when input value is an empty string - the pre-search state.
SearchResultsHeader
Rendered by SearchResults
.The default component renders tags that determine what search source results will be displayed.
SearchSourceResults
Rendered by SearchResults
. The component renders the UI components for specific search source listing:
SearchSourceResultsHeader
- the default component does not render any markup. Can be used to add information about the source which items are being rendered in the listing below.SearchSourceResultsEmpty
- rendered instead ofSearchSourceResultList
SearchSourceResultList
- renders items for a given search source
SearchSourceResultList
This is a child component of SearchSourceResults
component. Renders a list of items in an InfiniteScrollPaginator
component. Allows to specify React components for rendering search source items of a given type.
SearchSourceResultListFooter
- component rendered at the bottom ofSearchSourceResultList
. The default component informs user that more items are being loaded (SearchSourceResultsLoadingIndicator
) or that there are no more items to be loaded.SearchSourceResultsLoadingIndicator
- rendered bySearchSourceResultListFooter
Contexts
Search context
The main container component - Search
- provides search context to child components.
directMessagingChannelType
The type of channel to create on user result select, defaults to messaging
. This is just a forwarded value of Search
component’s directMessagingChannelType
prop.
Type | Default |
---|---|
string | ’messaging’ |
disabled
Sets the input element into disabled state. This is just a forwarded value of Search
component’s disabled
prop.
Type |
---|
boolean |
exitSearchOnInputBlur
Clear search state / search results on every click outside the search input. By default, the search UI is not removed on input blur. This is just a forwarded value of Search
component’s exitSearchOnInputBlur
prop.
Type |
---|
boolean |
placeholder
Custom placeholder text to be displayed in the search input. This is just a forwarded value of Search
component’s placeholder
prop.
Type |
---|
string |
searchController
Instance of the SearchController class that handles the data management. This is just a forwarded value of Chat
component’s searchController
prop. The child components can access the searchController
state in a reactive manner.
import {
SearchSourceResults,
SearchResultsHeader,
SearchResultsPresearch,
useSearchContext,
useStateStore,
} from "stream-chat-react";
import type { SearchControllerState } from "stream-chat";
const searchControllerStateSelector = (nextValue: SearchControllerState) => ({
activeSources: nextValue.sources.filter((s) => s.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;
};
Search source context
The context is rendered by SearchSourceResults
component. It provides the instance of search source class corresponding to the specified type. The child components can access the instance’s reactive state to render the data:
import {
useSearchSourceResultsContext,
useStateStore,
} from "stream-chat-react";
import type { SearchSourceState } from "stream-chat";
const searchSourceStateSelector = (value: SearchSourceState) => ({
hasMore: value.hasMore,
isLoading: value.isLoading,
});
const Component = () => {
const { searchSource } = useSearchSourceResultsContext();
const { hasMore, isLoading } = useStateStore(
searchSource.state,
searchSourceStateSelector,
);
return (
<div>
{isLoading ? (
<div>Is loading</div>
) : !hasMore ? (
<div>All results loaded</div>
) : null}
</div>
);
};