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 the WithComponents component

image

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:

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 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 by name partially matching the search query
  • UserSearchSource - queries users by name or id partially matching the search query
  • MessageSearchSource - queries messages and corresponding channels by message text 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<StreamChatGenerics>({
    apiKey,
    tokenOrProvider: userToken,
    userData: { id: userId },
  });

  // create a memoized instance of SearchController
  const searchController = useMemo(
    () =>
      chatClient
        ? new SearchController<StreamChatGenerics>({
            sources: [
              new XSearchSource(),
              new ChannelSearchSource<StreamChatGenerics>(chatClient),
              new UserSearchSource<StreamChatGenerics>(chatClient),
              new MessageSearchSource<StreamChatGenerics>(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 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.

The top-level component for rendering SearchBar and SearchResults

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 of SearchSourceResultList
  • 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 of SearchSourceResultList. The default component informs user that more items are being loaded (SearchSourceResultsLoadingIndicator) or that there are no more items to be loaded.
  • SearchSourceResultsLoadingIndicator - rendered by SearchSourceResultListFooter

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.

TypeDefault
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<StreamChatGenerics>();
  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>
  );
};
© Getstream.io, Inc. All Rights Reserved.