Info:
This is release candidate documentation for Stream Chat React SDK v15. For the latest stable version, see the latest version (v14).

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;
    • SearchSourceResults for each active source: SearchSourceResultsHeader, which renders nothing by default, then SearchSourceResultList with the results, or SearchSourceResultsEmpty when the source found nothing.
  • SearchSourceResultList renders the results in an infinite scroll, with SearchSourceResultListFooter and its SearchSourceResultsLoadingIndicator at 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>
);

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