Search customization

The search bar above the channel list finds channels, people and messages. This recipe changes how it is set up, how it looks and where it sits. How search works across the layers, and what each result does when clicked, is on Search.

Note:

What you'll build

  • Search bar settings of your own, such as its placeholder
  • Compact channel results with name and member count
  • Search in a top bar instead of the sidebar

Preview

Channel results in the sidebar search shown as compact rows with the channel name and member count

Approach: Replace one part and Compose. Choosing where to start explains the approaches.

Starts from: the base app. The page shows only what the recipe adds.

Best practices

  • Override the smallest part you need: Search to set its props, SearchSourceResultList to change one source's results, SearchBar or SearchResults for the rest.
  • Start from DefaultSearchResultItems and replace one entry, so the other sources keep their default results.
  • Keep what a result does when clicked. A channel result opens the channel, watches it, and adds it to the channel lists that match it.
  • Keep role="option" on a custom result's button. The arrow keys move between the results that have it.

Configure search through WithComponents

ChannelNavigation renders the channel list header, the search bar and the channel lists. It renders search from the Search key, so to change the search bar's props, set a component on that key that renders Search with them:

src/SidebarSearch.tsx
import type { PropsWithChildren } from "react";
import { Search, WithComponents } from "stream-chat-react";

const SidebarSearch = () => (
  <Search
    directMessagingChannelType="team"
    exitSearchOnInputBlur
    placeholder="Search teammates, channels and messages"
  />
);

// Declared once, so WithComponents gets the same object on every render.
const overrides = { Search: SidebarSearch };

export const WithSidebarSearch = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={overrides}>{children}</WithComponents>
);

Wrap ChannelNavigation in WithSidebarSearch, or the ChatView that renders it:

<WithSidebarSearch>
  <ChatView layouts={layouts} views={views} />
</WithSidebarSearch>
  • directMessagingChannelType is the channel type a user result opens; the default is messaging.
  • exitSearchOnInputBlur closes search when focus moves from the input to something outside search, such as a click elsewhere.
  • placeholder, disabled and inputProps go to the input.

The key applies only where the SDK renders search. A Search you render yourself takes the props directly, as in Render search outside ChannelList.

Customize search result items

SearchSourceResultList renders the results of one source. Its SearchResultItems prop maps each source's type (channels, users, messages) to the component that renders one result. This list renders channel results as compact rows with the channel's name and member count, and keeps the default user and message results:

src/CompactChannelResults.tsx
import type { PropsWithChildren } from "react";
import type { Channel, ChannelStateData } from "stream-chat";
import {
  DefaultSearchResultItems,
  SearchSourceResultList,
  useChannelDisplayName,
  useChatContext,
  useStateStore,
  useWorkspaceNavigation,
  WithComponents,
} from "stream-chat-react";
import "./CompactChannelResults.css";

const memberCountSelector = ({ memberCount }: ChannelStateData) => ({
  memberCount,
});

const CompactChannelResult = ({ item }: { item: Channel }) => {
  const { channelManager, client } = useChatContext();
  const { openChannel } = useWorkspaceNavigation();
  const name = useChannelDisplayName(item);
  const { memberCount } = useStateStore(item.state, memberCountSelector);

  return (
    <button
      className="cookbook-channel-result"
      onClick={(event) => {
        openChannel(item, { event });
        // Channel search doesn't watch its results, so watch the channel to receive its events.
        item.watch().catch((error: Error) =>
          client.notifications.addError({
            message: "Failed to load the channel",
            options: { originalError: error },
            origin: { emitter: "CompactChannelResult" },
          }),
        );
        // Add the channel to the channel lists whose filters it matches.
        channelManager.ingestChannel(item);
      }}
      role="option"
      type="button"
    >
      <span className="cookbook-channel-result__name"># {name}</span>
      <span className="cookbook-channel-result__members">
        {memberCount} members
      </span>
    </button>
  );
};

const searchResultItems = {
  ...DefaultSearchResultItems,
  channels: CompactChannelResult,
};

const ResultList = () => (
  <SearchSourceResultList SearchResultItems={searchResultItems} />
);

// Declared once, so WithComponents gets the same object on every render.
const overrides = { SearchSourceResultList: ResultList };

export const WithCompactChannelResults = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={overrides}>{children}</WithComponents>
);

The rules sit in the stream-overrides layer, so they win over the SDK's own rules once the SDK's stylesheet is in an earlier layer, as Theming → CSS layers sets up. They take their colors, spacing and type from the SDK's tokens, so they follow your theme. Wrap the part of the app that renders search in WithCompactChannelResults, as with WithSidebarSearch above.

To keep the default result and only add to it, wrap it instead. ChannelSearchResultItem, UserSearchResultItem and MessageSearchResultItem are exported, and each takes onSelect to replace its click. The Search feature page wraps the default user result this way.

Render search outside ChannelList

Search reads the search controller from Chat, so it works anywhere inside Chat. This workspace puts it in a top bar and keeps the channel lists in the sidebar:

src/Workspace.tsx
import {
  ChannelHeader,
  ChannelLists,
  MessageComposer,
  MessageList,
  Search,
  ThreadHeader,
} from "stream-chat-react";
import { ChannelSlot, ThreadSlot } from "stream-chat-react/slot-layout";
import type { LayoutDescriptor } from "stream-chat-react/slot-layout";
import "./Workspace.css";

export const layouts: LayoutDescriptor[] = [
  { id: "channels", slots: ["channel", "thread"] },
];

export const ChannelsView = () => (
  <div className="cookbook-workspace">
    <header className="cookbook-workspace__top-bar">
      <Search placeholder="Search everything" />
    </header>
    <div className="cookbook-workspace__body">
      <aside className="cookbook-workspace__sidebar">
        <ChannelLists />
      </aside>
      <ChannelSlot slot="channel">
        <ChannelHeader />
        <MessageList />
        <MessageComposer />
      </ChannelSlot>
      <ThreadSlot slot="thread">
        <ThreadHeader />
        <MessageList />
        <MessageComposer />
      </ThreadSlot>
    </div>
  </div>
);

Render ChannelsView as the channels view of ChatView:

<ChatView layouts={layouts} views={{ channels: <ChannelsView /> }} />

The rules sit in the stream-overrides layer, so they win over the SDK's own rules once the SDK's stylesheet is in an earlier layer, as Theming → CSS layers sets up. They take their colors, spacing and type from the SDK's tokens, so they follow your theme.

ChannelLists renders every list registered on client.channelManager. Unlike ChannelNavigation, it doesn't hide the lists while search is open, so the sidebar stays as it is and the results show in the top bar. The channels view lays out its children in a row, so ChannelsView puts them in a column of its own: the top bar, then a row with the lists, the channel and the thread. The results render inside Search, under its search bar, and the SDK has no setting that opens them as a dropdown, so the CSS positions them over the content below. Workspace with ChatView explains layouts, views and slots.


Results under the top bar

Search results open as a dropdown under a search bar that spans the top of the app

Go lower-level when you need custom data sources

The components above change how search looks. What it searches comes from the SearchController that Chat holds, with one source per kind of result:

  • To search fewer kinds of results, or to change a source's filters, pass your own controller to Chat as searchController. The Search feature page shows how, under "Choosing what to search".
  • To search your own data, such as projects or documents, write a source as Adding your own source describes, add it to the controller, and map its type in SearchResultItems as above.

How it works

Info:

Example

The user types "des" in the sidebar search, with WithCompactChannelResults around the app, and clicks Design review:

  1. Search passes the text to the search controller that Chat holds, and the results replace the channel lists in the sidebar.
  2. SearchSourceResultList renders the channel results with CompactChannelResult, which shows each channel's name and member count. The people and message results render with the defaults from DefaultSearchResultItems.
  3. The user clicks Design review. WorkspaceNavigation.openChannel() hands the channel to the layout, which decides where it opens. The event is passed on, so ⌘/ctrl-click can open it beside the current one.
  4. Channel.watch() watches the channel, since channel search doesn't watch its results. A failed watch publishes an error notification, as the default does.
  5. ChannelManager.ingestChannel() adds the channel to the registered channel lists that match it, so it shows in the list without a new query.

Steps 3 to 5 are what the default channel result does, so the compact result opens a channel the same way.