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

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 Chat as searchController.
  • 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 Search props on the Search that ChannelNavigation renders, register a component that renders Search with them under the Search key.

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.