# Search

The React SDK’s default search is handled by [`ChannelSearch`](/chat/docs/sdk/react/v13/components/utility-components/channel_search/), but it has limitations:

- You can’t easily change what’s searched or which endpoints are used (logic lives in the `useChannelSearch` hook, with limited customization via prop callbacks).
- UI customization requires replacing the entire `ChannelSearch` component.

The experimental `Search` component addresses this by:

- Using a `SearchController` for customizable search logic.
- Allowing UI component customization via `ComponentContext` and `WithComponents`.

## Best Practices

- Use the experimental `Search` only if default `ChannelSearch` is insufficient.
- Keep search sources scoped to your use case to avoid noisy results.
- Memoize `SearchController` instances to prevent reset churn.
- Use `FilterBuilder` for dynamic filters instead of manual query hacks.
- Re-test search UI after SDK upgrades due to experimental changes.

![image](@chat-sdk/react/v13/_assets/experimental-search-ui.png)

## Basic Usage

To replace the rendering of `ChannelSearch` by `Search`, import it from the experimental package and create a `ComponentContext` override around the `ChannelList` that will render `Search`:

```tsx
import { StreamChat } from "stream-chat";
import { Chat, ChannelList, WithComponents } from "stream-chat-react";
import { Search } from "stream-chat-react/experimental";

const Component = () => (
  // [!code word:overrides={{ Search }}]
  <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

Search state is managed by the reactive `SearchController` class (exported by `stream-chat`). It uses search sources for data retrieval and pagination. The default sources are:

- `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

You can customize the existing sources or add new ones for custom entities.

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.

```tsx
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({
    apiKey,
    tokenOrProvider: userToken,
    userData: { id: userId },
  });

  // create a memoized instance of SearchController
  const searchController = useMemo(
    () =>
      chatClient
        ? new SearchController({
            sources: [
              new XSearchSource(),
              new ChannelSearchSource(chatClient),
              new UserSearchSource(chatClient),
              new MessageSearchSource(chatClient),
            ],
          })
        : undefined,
    [chatClient],
  );

  if (!chatClient) return <>Loading...</>;

  return (
    <Chat client={chatClient} searchController={searchController}>
      <WithComponents
        overrides={{
          Search,
          SearchSourceResultList: CustomSearchResultList,
        }}
      >
        {/*  ....*/}
      </WithComponents>
    </Chat>
  );
};
```

You can override query parameters like `filters`, `sort`, or `searchOptions` by assignment:

```tsx
const { searchController } = useChatContext();

const usersSearchSource = searchController.getSource("users");
usersSearchSource.filters = {
  ...usersSearchSource.filters,
  myCustomField: "some-value",
};
```

## Search Request Dynamic Filter Building

<admonition type="info">

It is possible to build request filters dynamically as of `stream-chat@9.17.0`.

</admonition>

Default search sources include `FilterBuilder` instances for dynamic filter building.

The default search sources count with at least one filter builder:

1. `ChannelSearchSource` has a single FilterBuilder instance, that can be accessed with `channelSearchSource.filterBuilder`
2. `UserSearchSource` has a single FilterBuilder instance, that can be accessed with `userSearchSource.filterBuilder`
3. `MessageSearchSource` has three FilterBuilder instances as three different filters has to be built. These can be accessed as:

   a. `messageSearchSource.messageSearchChannelFilterBuilder` - builds filters for the first argument to `client.search()`

   b. `messageSearchSource.messageSearchFilterBuilder` - builds filters for the second argument to `client.search()`

   c. `messageSearchSource.channelQueryFilterBuilder` - builds filters for the first argument to `client.queryChannels()` (retrieves the channels corresponding to the messages returned from `client.search()`)

It is possible to define the custom filter config at the search source initiation. In that case, we need to create `SearchController` instance too. Alongside `initialFilterConfig` we can define `initialContext`, which represents custom fields that we want to have available at the filter build time. Besides the custom context fields, every search source counts with field `searchQuery` as a part of the filter builder context.

We can register filter generators via `initialFilterConfig` as follows:

1. register the configuration object under a key that will allow us to target that filter builder
2. determine whether the filter is enabled at the beginning
3. determine the generator logic

It is important to note that the generator functions should:

- **return the partial filter object that is internally merged into the resulting filter** or
- `null` which would signal the partial from the given generator should to be ignored

An example would be

```ts
// generic type determines the custom fields for the filter builder context available in the generator functions
new ChannelSearchSource<{ archived: boolean }>(
  //... previous args
  {
    // initial context does not contain searchQuery as that is handled by the search source at the runtime
    initialContext: { archived: false },
    initialFilterConfig: {
      memberUserName: {
        // 1. register the configuration object under a key
        enabled: true, // 2. determine whether the filter is enabled at the beginning
        // the archived custom context property is available along with the default searchQuery
        generate: (
          { archived, searchQuery }, // 3. determine the generator logic
        ) =>
          searchQuery
            ? {
                "member.user.name": { $autocomplete: searchQuery },
              }
            : null,
      },
      //...  other filters for ChannelSearchSource
    },
  },
);
```

An example of creating a SearchController with custom filter generator configuration could look as follows:

```ts
const useCustomSearchController = (chatClient: StreamChat | null) => {
  // create a memoized instance of SearchController
  return useMemo(
    () =>
      chatClient
        ? new SearchController({
            sources: [
              new ChannelSearchSource<{ archived: boolean }>(
                chatClient,
                {}, // default search options are kept, but the empty object has to be provided due to positional arguments
                {
                  initialContext: { archived: false },
                  initialFilterConfig: {
                    archived: {
                      enabled: true,
                      generate: ({ archived }) => ({
                        archived,
                      }),
                    },
                    memberUserName: {
                      enabled: true,
                      generate: ({ searchQuery }) =>
                        searchQuery
                          ? {
                              "member.user.name": {
                                $autocomplete: searchQuery,
                              },
                            }
                          : null,
                    },
                    custom_loading_places_country_codes: {
                      enabled: false,
                      generate: ({ searchQuery }) =>
                        searchQuery
                          ? {
                              // custom property has to be declared
                              custom_loading_places_country_codes: {
                                $in: [searchQuery],
                              },
                            }
                          : null,
                    },
                  },
                },
              ),
              new UserSearchSource(
                chatClient,
                {}, // default search options are kept, but the empty object has to be provided due to positional arguments
                {
                  //   again custom initialContext and (or) initialFilterConfig
                },
              ),
              new MessageSearchSource<{
                // context definition for each filter builder
                messageSearchChannelContext: { a: string };
                messageSearchContext: { b: string };
                channelQueryContext: { c: string };
              }>(
                client,
                {}, // default search options are kept, but the empty object has to be provided due to positional arguments
                {
                  messageSearchChannelFilterBuilder: {
                    initialContext: { a: "messageSearchChannelFilterBuilder" },
                    initialFilterConfig: {
                      custom: {
                        enabled: true,
                        generate: ({ searchQuery, a }) =>
                          searchQuery
                            ? { name: { $autocomplete: searchQuery + a } }
                            : null,
                      },
                    },
                  },
                  messageSearchFilterBuilder: {
                    initialContext: { b: "messageSearchFilterBuilder" },
                    initialFilterConfig: {
                      text: {
                        enabled: true,
                        generate: ({ searchQuery, b }) =>
                          searchQuery ? { text: searchQuery + b } : null,
                      },
                    },
                  },
                  channelQueryFilterBuilder: {
                    initialContext: { c: "channelQueryFilterBuilder" },
                    initialFilterConfig: {
                      cid: {
                        enabled: true,
                        generate: ({ cids, c }) =>
                          cids
                            ? { cid: { $in: cids.concat([c as string]) } }
                            : null,
                      },
                    },
                  },
                },
              ),
            ],
          })
        : undefined,
    [chatClient],
  );
};
```

See our [guide on declaring custom fields](/chat/docs/sdk/react/v13/guides/typescript_and_custom_data_types) for entities like channels, messages, attachments etc.

It is possible to disable or enable back the individual filter generators:

```js
const searchSource = new ChannelSearchSource(client);

// disables the default filter called name
searchSource.filterBuilder.disableFilter("name");
// enables the default filter called name
searchSource.filterBuilder.enableFilter("name");
```

It is possible to adjust the filter context

```js
const searchSource = new ChannelSearchSource(client);

// Merges the archived key with the rest of the context. Existing key is overidden.
searchSource.filterBuilder.updateContext({ archived: true });
```

And finally putting it all together within a React component, we can see that `searchSource.filterBuilder.context` and `searchSource.filterBuilder.filterConfig` are reactive. That means we can subscribe and react to context being updated or filters being enabled / disabled:

```tsx
import {
  ChannelFilters,
  ChannelSearchSource,
  ChannelSearchSourceFilterBuilderContext,
  FilterBuilderGenerators,
} from "stream-chat";
import { useSearchContext, useStateStore } from "stream-chat-react";

const contextSelector = ({
  archived,
}: ChannelSearchSourceFilterBuilderContext<{ archived: boolean }>) => ({
  archived,
});

const filterGeneratorEnablementSelector = (
  generators: FilterBuilderGenerators<
    ChannelFilters,
    ChannelSearchSourceFilterBuilderContext<{
      archived: boolean;
    }>
  >,
) => ({
  custom_loading_places_country_codes:
    generators.custom_loading_places_country_codes.enabled,
});

const SearchSourceFilterButton = () => {
  // activates individual search sources
};

export const SearchResultsHeader = () => {
  const { searchController } = useSearchContext();
  const channelSearchSource = (searchController.getSource("channels") ??
    {}) as ChannelSearchSource<{ archived: boolean }>;
  const { archived } = useStateStore(
    channelSearchSource.filterBuilder?.context,
    contextSelector,
  );
  const { custom_loading_places_country_codes } = useStateStore(
    channelSearchSource.filterBuilder?.filterConfig,
    filterGeneratorEnablementSelector,
  );

  return (
    <div
      className="str-chat__search-results-header"
      data-testid="search-results-header"
    >
      <div
        className="str-chat__search-results-header__filter-source-buttons"
        data-testid="filter-source-buttons"
      >
        {searchController.sources.map((source) => (
          <SearchSourceFilterButton
            key={`search-source-filter-button-${source.type}`}
            source={source}
          />
        ))}
        <label>
          Archived
          <input
            onClick={() =>
              channelSearchSource?.filterBuilder.updateContext({
                archived: !archived,
              })
            }
            type="checkbox"
            value={archived.toString()}
          />
        </label>
        <label>
          custom_loading_places_country_codes
          <input
            onClick={() =>
              custom_loading_places_country_codes
                ? channelSearchSource?.filterBuilder.disableFilter(
                    "custom_loading_places_country_codes",
                  )
                : channelSearchSource?.filterBuilder.enableFilter(
                    "custom_loading_places_country_codes",
                  )
            }
            type="checkbox"
            value={custom_loading_places_country_codes.toString()}
          />
        </label>
      </div>
    </div>
  );
};
```

## Search UI Components And Their Customization

The default search UI components can be overridden through `ComponentContext`, using the default component names. There are branch components that render other components and leaf components that render the markup.

### Search

The top-level component for rendering `SearchBar` and `SearchResults`

### SearchBar

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

### SearchContext

The main container component, `Search`, provides `SearchContext` 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.

| Type   | Default     |
| ------ | ----------- |
| 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.

```tsx
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();
  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;
};
```

### SearchSourceContext

`SearchSourceResults` renders `SearchSourceContext`. It provides the search source instance for the specified type. Child components can access the instance's reactive state to render data:

```tsx
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>
  );
};
```


---

This page was last updated at 2026-04-22T16:43:04.019Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v13/experimental/search/](https://getstream.io/chat/docs/sdk/react/v13/experimental/search/).