# Advanced Search

[`Search`](/chat/docs/sdk/react/components/utility-components/channel_search/) covers the common sidebar-search use case, but the underlying `SearchController` is also customizable when you need to control sources, filters, ranking, or result rendering.

## Best Practices

- Keep advanced search logic in a memoized `SearchController`.
- Use custom sources for data changes and `WithComponents` for UI changes.
- Reuse `DefaultSearchResultItems` when you only want to replace one source renderer.
- Prefer `FilterBuilder` or source filters over ad hoc query logic in React components.
- Keep advanced search close to `Chat` so all search consumers share the same controller.

## Basic Usage

To customize the default search surface used by `ChannelList`, override `Search` from the main package:

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

const SidebarSearch = () => (
  <Search
    exitSearchOnInputBlur
    placeholder="Search channels, messages, and users..."
  />
);

const App = () => (
  <Chat client={client}>
    <WithComponents overrides={{ Search: SidebarSearch }}>
      <ChannelList
        filters={filters}
        options={options}
        showChannelSearch
        sort={sort}
      />
    </WithComponents>
  </Chat>
);
```

## Search Data Management

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

- `ChannelSearchSource` for channels
- `UserSearchSource` for users
- `MessageSearchSource` for messages

### Customizing Search Data Retrieval

You can customize the built-in sources or add your own source for a custom entity. The same pattern applies whether you are introducing a brand-new source type or replacing one of the defaults.

Each custom source needs:

1. a unique `type`
2. a `query()` implementation
3. an optional `filterQueryResults()` implementation

The general pattern is:

1. create a source class that extends `BaseSearchSource<T>`
2. return `{ items }` from `query()`
3. register the source in `SearchController`
4. provide a matching result-item component in `SearchSourceResultList`

### Example: Build A Custom Search Source

The example below demonstrates how to add a new custom search source. This is the most common pattern when your search needs to include an app-specific entity that Stream Chat does not query by default.

The important pieces are:

1. the source class extends `BaseSearchSource<T>`
2. the class declares a stable `type`
3. `query()` returns the page of items for the current search string
4. `filterQueryResults()` can optionally reshape that page before the UI sees it
5. `SearchSourceResultList` receives a `SearchResultItems` map so the UI knows how to render the new source type

```tsx
import { useMemo } from "react";
import {
  BaseSearchSource,
  ChannelSearchSource,
  MessageSearchSource,
  SearchController,
  SearchSourceOptions,
  UserSearchSource,
} from "stream-chat";
import {
  Chat,
  DefaultSearchResultItems,
  Search,
  SearchSourceResultList,
  WithComponents,
} from "stream-chat-react";

type Project = { id: string; name: string };

class ProjectSearchSource extends BaseSearchSource<Project> {
  // the search source type is used as the source key in SearchController
  readonly type = "projects";

  constructor(options?: SearchSourceOptions) {
    super(options);
  }

  // query() always receives the current search string
  protected async query(searchQuery: string) {
    return searchQuery.length > 1
      ? { items: [{ id: "alpha", name: `Project ${searchQuery}` }] }
      : { items: [] };
  }

  // filterQueryResults() is optional; use it when you want to adjust the retrieved page
  protected filterQueryResults(items: Project[]) {
    return items;
  }
}

// this component renders one item for the custom source
const ProjectResult = ({ item }: { item: Project }) => (
  <button type="button">{item.name}</button>
);

// SearchSourceResultList needs to know which component renders each source type
const SearchResultList = () => (
  <SearchSourceResultList
    SearchResultItems={{
      ...DefaultSearchResultItems,
      projects: ProjectResult,
    }}
  />
);

const App = () => {
  const searchController = useMemo(() => {
    if (!client) return;
    return new SearchController({
      sources: [
        new ProjectSearchSource(),
        new ChannelSearchSource(client),
        new UserSearchSource(client),
        new MessageSearchSource(client),
      ],
    });
  }, [client]);

  if (!searchController) return null;

  return (
    <Chat client={client} searchController={searchController}>
      <WithComponents
        overrides={{ Search, SearchSourceResultList: SearchResultList }}
      >
        <ChannelList
          filters={filters}
          options={options}
          showChannelSearch
          sort={sort}
        />
      </WithComponents>
    </Chat>
  );
};
```

That same pattern can be used to override a default source as well. In that case, you would still create the source instance yourself and register it in `SearchController`, but you would use `ChannelSearchSource`, `UserSearchSource`, or `MessageSearchSource` instead of extending `BaseSearchSource`.

### Example: Replace A Built-In Source

Sometimes you do not need a brand-new source type. You just need the existing channel or user source to query with different defaults. In that case, instantiate the built-in source yourself and pass it into `SearchController`.

```tsx
import { useMemo } from "react";
import {
  ChannelSearchSource,
  SearchController,
  UserSearchSource,
} from "stream-chat";

const useCustomSearchController = (client: StreamChat | null, userId: string) =>
  useMemo(() => {
    if (!client) return undefined;

    const channelSearchSource = new ChannelSearchSource(client, {
      filters: { members: { $in: [userId] }, type: "messaging" },
    });

    const userSearchSource = new UserSearchSource(client, {
      filters: { id: { $ne: userId } },
    });

    return new SearchController({
      sources: [channelSearchSource, userSearchSource],
    });
  }, [client, userId]);
```

This approach keeps the SDK's built-in search-source behavior, pagination, and result types, but swaps in your own query configuration.

You can also override query parameters such as `filters`, `sort`, or `searchOptions` by assignment:

```tsx
import { useChatContext } from "stream-chat-react";

const SearchSourceFilters = () => {
  const { searchController } = useChatContext();

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

  return null;
};
```

## Search Request Dynamic Filter Building

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

The built-in sources expose these filter builders:

1. `ChannelSearchSource` has `channelSearchSource.filterBuilder`
2. `UserSearchSource` has `userSearchSource.filterBuilder`
3. `MessageSearchSource` has:
   - `messageSearchSource.messageSearchChannelFilterBuilder`
   - `messageSearchSource.messageSearchFilterBuilder`
   - `messageSearchSource.channelQueryFilterBuilder`

You can define `initialFilterConfig` and `initialContext` when creating a source. The generator functions should return either:

- a partial filter object that gets merged into the final filter
- `null` to skip that generator

```tsx
import { ChannelSearchSource } from "stream-chat";

const channelSearchSource = new ChannelSearchSource<{ archived: boolean }>(
  client,
  undefined,
  {
    initialContext: { archived: false },
    initialFilterConfig: {
      archived: {
        enabled: true,
        generator: ({ archived }) => ({ archived }),
      },
      memberUserName: {
        enabled: true,
        generator: ({ searchQuery }) =>
          searchQuery
            ? { "member.user.name": { $autocomplete: searchQuery } }
            : null,
      },
    },
  },
);
```

When you need more than one source with custom filter builders, create the `SearchController` explicitly:

```tsx
import { useMemo } from "react";
import {
  ChannelSearchSource,
  MessageSearchSource,
  SearchController,
  UserSearchSource,
} from "stream-chat";

const useCustomSearchController = (client: StreamChat | null) =>
  useMemo(() => {
    if (!client) return undefined;

    const channelSearchSource = new ChannelSearchSource<{ archived: boolean }>(
      client,
      undefined,
      {
        initialContext: { archived: false },
        initialFilterConfig: {
          archived: {
            enabled: true,
            generator: ({ archived }) => ({ archived }),
          },
        },
      },
    );

    const userSearchSource = new UserSearchSource(client);

    const messageSearchSource = new MessageSearchSource<{
      messageSearchChannelContext: { region: string };
      messageSearchContext: { onlyPinned: boolean };
      channelQueryContext: { includeArchived: boolean };
    }>(client);

    return new SearchController({
      sources: [channelSearchSource, userSearchSource, messageSearchSource],
    });
  }, [client]);
```

It is also possible to enable or disable individual generators at runtime:

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

searchSource.filterBuilder.disableFilter("name");
searchSource.filterBuilder.enableFilter("name");
```

And you can update the filter-builder context reactively:

```tsx
const searchSource = new ChannelSearchSource<{ archived: boolean }>(client);

searchSource.filterBuilder.setContext({ archived: true });
```

That reactive state can be consumed from your custom search UI:

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

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

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

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

  return (
    <div className="str-chat__search-results-header">
      <label>
        Archived
        <input
          checked={archived}
          type="checkbox"
          onChange={() =>
            channelSearchSource.filterBuilder.setContext({
              archived: !archived,
            })
          }
        />
      </label>
      <label>
        Enable archived filter
        <input
          checked={archivedFilterEnabled}
          type="checkbox"
          onChange={() =>
            archivedFilterEnabled
              ? channelSearchSource.filterBuilder.disableFilter("archived")
              : channelSearchSource.filterBuilder.enableFilter("archived")
          }
        />
      </label>
    </div>
  );
};
```

## Search UI Components And Their Customization

The default search UI components can be overridden through `ComponentContext`.

### Search

The top-level component that renders `SearchBar` and `SearchResults`.

### SearchBar

The input layer, clear button, and cancel button.

### SearchResults

The top-level results container for one or more active search sources.

#### SearchResultsPresearch

Rendered when search is active but the query is still empty.

#### SearchResultsHeader

Rendered above the source-specific results pane.

#### SearchSourceResults

Rendered once per active search source. It coordinates:

- `SearchSourceResultsHeader`
- `SearchSourceResultsEmpty`
- `SearchSourceResultList`

##### SearchSourceResultList

Renders the source items inside an `InfiniteScrollPaginator`. It can also receive a `SearchResultItems` map that decides how each source type should render.

- `SearchSourceResultListFooter` renders the footer area
- `SearchSourceResultsLoadingIndicator` renders the loading state within that footer

## Contexts

### SearchContext

`Search` provides `SearchContext` to its child components.

#### Values

| Value                        | Description                                                                   | Type               |
| ---------------------------- | ----------------------------------------------------------------------------- | ------------------ |
| `directMessagingChannelType` | The channel type to create on user-result selection. Defaults to `messaging`. | `string`           |
| `disabled`                   | Whether the input is disabled.                                                | `boolean`          |
| `exitSearchOnInputBlur`      | Whether the empty search UI should exit on input blur.                        | `boolean`          |
| `placeholder`                | Placeholder text forwarded from `Search`.                                     | `string`           |
| `searchController`           | The `SearchController` instance from `Chat`.                                  | `SearchController` |

You can consume the `searchController` reactively:

```tsx
import {
  SearchResultsHeader,
  SearchResultsPresearch,
  SearchSourceResults,
  useSearchContext,
  useStateStore,
} from "stream-chat-react";
import type { SearchControllerState } from "stream-chat";

const searchControllerStateSelector = (nextValue: SearchControllerState) => ({
  activeSources: nextValue.sources.filter((source) => source.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;
};
```

### SearchSourceResultsContext

`SearchSourceResults` provides `useSearchSourceResultsContext()` for the active source pane. That lets child components react to source-level state such as pagination or loading:

```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 SearchSourceFooter = () => {
  const { searchSource } = useSearchSourceResultsContext();
  const { hasMore, isLoading } = useStateStore(
    searchSource.state,
    searchSourceStateSelector,
  );

  return (
    <div>
      {isLoading ? (
        <div>Loading…</div>
      ) : !hasMore ? (
        <div>All results loaded</div>
      ) : null}
    </div>
  );
};
```


---

This page was last updated at 2026-04-17T17:33:51.444Z.

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