Search
Search finds channels, people and messages. In the React Native SDK, search lives in the state layer: stream-chat provides a SearchController and one search source per kind of result, and the SDK's own screens use sources where they search. The SDK has no search screen for channels, users or messages. This page covers where the SDK searches, what it leaves to your app, and how to build a search screen from the sources. How the sources query and paginate is on Search sources.
What users get
Out of the box, users search in these places:
- Channel details. The full member list has a search field that narrows the members by name. The Add form searches users by name to add them. The Pinned Messages list has a search field that matches the text of the pinned messages. See Channel details and members.
- The composer. Typing
@suggests people to mention, and/suggests commands. Each list loads as the user types. See Suggestions.
The SDK doesn't have:
- a search field above the channel list, or a
Searchcomponent; - a screen of channel, user or message results, or components for those results;
- a
SearchControllerof its own.Chatcreates none and takes none.
To let users search their channels, people or messages, build the screen from the sources, as Building a search screen does.
How it works across the layers
State: the search sources
A search source holds the results of one kind of query, page by page, in its state store. A SearchController holds the search text and passes it to the sources that are active. 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.
stream-chat provides four sources for chat data:
| Source | Searches |
|---|---|
ChannelSearchSource |
The user's channels whose name matches the text. The results aren't watched. |
UserSearchSource |
Users whose id or name matches the text, without the current user. |
MessageSearchSource |
Messages that contain the text, in the user's channels, newest first. |
ChannelMemberSearchSource |
The members of one channel whose name matches the text, or whose id equals it. |
UI: where the SDK searches
Each place that searches creates its own source when it mounts, and takes yours instead as searchSource:
| Where | Source | Takes searchSource |
|---|---|---|
| The member list | ChannelMemberSearchSource, matching name, sorted by name |
ChannelMemberList, ChannelMemberListProvider |
| The Add form | UserSearchSource, matching name, sorted by name |
ChannelAddMembersForm, ChannelAddMembersProvider |
| Pinned Messages | MessageSearchSource, limited to the channel and pinned: true, matching text |
PinnedMessageList, ChannelPinnedMessageListProvider |
| Photos & Videos, and Files | MessageSearchSource, limited to the channel and the attachment types |
MediaList, FileAttachmentList, and their providers |
| Composer suggestions | The mentions and commands sources of the message composer | The composer's middleware, see Suggestions |
The channel details sources load 25 items a page, query with an empty text too, so the lists fill before the user types, and keep the current results on screen while a new text loads. The search field they render is SearchInput, which is exported for screens of your own.
Opening a result
On a screen of your own, each kind of result opens differently:
- A channel result is a
Channelthe search loaded with its latest messages without watching it. Pass it to your channel screen.Channelwatches it when it mounts, so the open channel receives events. - A message result is a
MessageResponsewith itscid.MessageSearchSourceloads the channels of its results intoclient.channelManagerwithout watching them, soclient.channelManager.get(message.cid)returns the channel. Open the channel and jump to the message, as Jumping when a screen opens shows. - A user result is a
UserResponse. Get the direct message channel withclient.channel()and the two members, and open it.Channelwatches it, which creates it on the server when it doesn't exist yet.
import type { StreamChat, UserResponse } from "stream-chat";
// The direct message channel between the current user and `user`.
export const getDirectMessageChannel = (
client: StreamChat,
user: UserResponse,
) =>
client.channel("messaging", {
members: [{ user_id: client.userId ?? "" }, { user_id: user.id }],
});Turning it on or off
The search fields in channel details and the composer suggestions are always on where those screens render. Search for channels, users and messages is on wherever your app renders a search screen.
The SDK checks no capabilities for search. The server decides what each query returns, and message search covers only the channel types whose search setting is on; see Channel types.
Customizing
Building a search screen
This screen searches the user's messages. It creates a MessageSearchSource, passes the typed text to it, reads its state with useStateStore, and loads the next page as the user scrolls:
import { useEffect, useMemo, useState } from "react";
import {
ActivityIndicator,
FlatList,
Pressable,
StyleSheet,
Text,
TextInput,
View,
} from "react-native";
import { MessageSearchSource } from "stream-chat";
import type { MessageResponse, SearchSourceState } from "stream-chat";
import { useChatContext, useStateStore } from "stream-chat-react-native";
// Declared at module scope, so the subscription isn't recreated on every render.
const resultsSelector = ({
hasNext,
isLoading,
items,
}: SearchSourceState<MessageResponse>) => ({ hasNext, isLoading, items });
export const MessageSearchScreen = ({
onSelectMessage,
}: {
onSelectMessage: (message: MessageResponse) => void;
}) => {
const { client } = useChatContext();
const [text, setText] = useState("");
const source = useMemo(() => new MessageSearchSource(client), [client]);
const { hasNext, isLoading, items } = useStateStore(
source.state,
resultsSelector,
);
// An inactive source doesn't query, and releases the channels of its results.
useEffect(() => {
source.activate();
return () => {
source.cancelScheduledQuery();
source.deactivate();
};
}, [source]);
const onChangeText = (value: string) => {
setText(value);
// The source waits for the user to stop typing before it queries.
void source.search(value);
};
return (
<View style={styles.container}>
<TextInput
accessibilityLabel="Search messages"
onChangeText={onChangeText}
placeholder="Search messages"
style={styles.input}
value={text}
/>
<FlatList
data={items ?? []}
keyExtractor={(message) => message.id}
ListFooterComponent={isLoading ? <ActivityIndicator /> : null}
onEndReached={() => {
// Without a text, `search()` loads the next page of the current one.
if (hasNext && items?.length) void source.search();
}}
renderItem={({ item }) => (
<Pressable
accessibilityRole="button"
onPress={() => onSelectMessage(item)}
style={styles.row}
>
<Text style={styles.author}>
{item.user?.name ?? item.user?.id}
</Text>
<Text numberOfLines={2}>{item.text}</Text>
</Pressable>
)}
/>
</View>
);
};
const styles = StyleSheet.create({
author: { fontWeight: "600" },
container: { flex: 1 },
input: { borderBottomWidth: StyleSheet.hairlineWidth, padding: 16 },
row: { padding: 16 },
});onSelectMessage gets the message. Your app opens its channel, client.channelManager.get(message.cid), and jumps to it. To search channels or users on the same screen, add a ChannelSearchSource and a UserSearchSource, and let a SearchController pass the text to the one the user picks, as The search controller shows.
Create the source once per client, as useMemo does here. A new source on every render starts every search from scratch. To change what a source queries, such as its filters and sort, configure it; see Built-in sources.
Changing what channel details searches
Pass a source of your own as searchSource. The SDK uses it as it is, so activate it and set its sort yourself. The Add form renders from the ChannelAddMembersForm key, so this override makes it search only users whose role is user:
import { useState } from "react";
import { UserSearchSource } from "stream-chat";
import {
ChannelAddMembersForm,
useChatContext,
} from "stream-chat-react-native";
import type {
ChannelAddMembersFormProps,
ComponentOverrides,
} from "stream-chat-react-native";
const AddMembersForm = (props: ChannelAddMembersFormProps) => {
const { client } = useChatContext();
// Created once, when the form opens.
const [searchSource] = useState(() => {
const source = new UserSearchSource(client, {
allowEmptySearchString: true,
filters: { role: "user" },
pageSize: 25,
resetOnNewSearchQuery: false,
sort: [{ direction: 1, field: "name" }],
});
source.activate();
return source;
});
return <ChannelAddMembersForm {...props} searchSource={searchSource} />;
};
// Pass these to WithComponents around ChannelDetails.
export const addMembersOverrides: ComponentOverrides = {
ChannelAddMembersForm: AddMembersForm,
};The UserSearchSource matches the text against the user's id and name, and adds filters under that match.
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, and render its state the way the screen above renders the messages.
Reference
- Search sources: the controller, the sources and their state.
- Channel details lists: the
searchSourceprops of the member list, the Add form and the message lists. - Channel details contexts: the providers that hold each list's source.
- Buttons and inputs:
SearchInput. - Suggestions: the composer's mention and command search.