# Search Sources

Several SDK components query and paginate a list behind a search input. That work is done by a search source from `stream-chat`, and each of those components accepts a `searchSource` prop so you can supply your own instance instead of the pre-configured default.

This guide covers what the sources do, how to tune their request behavior, and how to replace one.

## Best practices

- Create the source once and memoize it, so a re-render does not throw away its state and pagination cursor.
- Pass query parameters (`filters`, `sort`, `searchOptions`) to the constructor, and assign them as properties only when changing them later.
- Prefer tuning the built-in source over writing your own; a custom source has to reimplement pagination and loading state.
- Forward the abort signal in a custom `query()` so a superseded request stops in flight.

## Which components take a search source

| Component                                                                                      | Source type                 | Default configuration                   |
| ---------------------------------------------------------------------------------------------- | --------------------------- | --------------------------------------- |
| [`ChannelMemberList`](https://getstream.io/chat/docs/sdk/react-native/ui-components/channel-member-list/)          | `ChannelMemberSearchSource` | autocompletes members by `name`         |
| [`ChannelAddMembersForm`](https://getstream.io/chat/docs/sdk/react-native/ui-components/channel-add-members-form/) | `UserSearchSource`          | autocompletes users by `name`           |
| [`PinnedMessageList`](https://getstream.io/chat/docs/sdk/react-native/ui-components/pinned-message-list/)          | `MessageSearchSource`       | fetches pinned messages                 |
| [`FileAttachmentList`](https://getstream.io/chat/docs/sdk/react-native/ui-components/file-attachment-list/)        | `MessageSearchSource`       | fetches `file` and `audio` attachments  |
| [`MediaList`](https://getstream.io/chat/docs/sdk/react-native/ui-components/media-list/)                           | `MessageSearchSource`       | fetches `image` and `video` attachments |

Each source exposes reactive state (`items`, `isLoading`, `hasNext`, `searchQuery`) through a `StateStore`, which is what drives the list and its loading indicator.

## Tuning search requests

Short queries match a large share of the dataset, which makes them the slowest and most expensive ones to run server-side. Sources handle that in two ways: the debounce interval depends on how long the query is, and a request that a newer query has replaced is cancelled rather than left to finish.

### Adaptive debounce

The interval is resolved on every keystroke, so a query moves between the two buckets as it grows or shrinks.

| Option                 | Default | Applies to                                             |
| ---------------------- | ------- | ------------------------------------------------------ |
| `shortQueryDebounceMs` | `500`   | queries of at most `shortQueryMaxLength` characters    |
| `longQueryDebounceMs`  | `300`   | queries longer than `shortQueryMaxLength`              |
| `shortQueryMaxLength`  | `2`     | the query length that still counts as short, inclusive |

```tsx
import { useMemo } from "react";
import { ChannelMemberSearchSource } from "stream-chat";
import { ChannelMemberList } from "stream-chat-react-native";

const MemberScreen = ({ channel }) => {
  const searchSource = useMemo(
    () =>
      new ChannelMemberSearchSource(channel, {
        longQueryDebounceMs: 250,
        pageSize: 20,
        shortQueryDebounceMs: 700,
        shortQueryMaxLength: 3,
      }),
    [channel],
  );

  return <ChannelMemberList searchSource={searchSource} />;
};
```

Typing `a`, `ab`, then `abc` schedules at 500ms, 500ms, and finally 300ms, and only the last one runs. Deleting back down to `ab` returns to the 500ms interval.

Setting the legacy `debounceMs` applies a single interval to both buckets, which opts out of the adaptive behavior:

```tsx
new ChannelMemberSearchSource(channel, { debounceMs: 400 });
```

The intervals can also be changed on a live source:

```tsx
searchSource.setDebounceOptions({ shortQueryDebounceMs: 600 });
```

Mobile networks make this worth tuning. A longer short-query interval trades first-keystroke latency for far fewer wasted requests on a slow connection.

### Request cancellation

When the debounce fires while a previous request is still in flight, the source aborts that request before dispatching the new one. A late response from the superseded query is discarded, so it can never overwrite newer results.

A new search query preempts an in-flight one rather than being dropped. Pagination still waits for the current page to arrive, so a load-more call is a no-op while a query is loading.

`resetState()` and `cancelScheduledQuery()` abort too. Call one of them when a screen holding a source unmounts, so a pending request does not outlive it:

```tsx
useEffect(() => () => searchSource.cancelScheduledQuery(), [searchSource]);
```

The built-in sources handle all of this already. A custom source participates by forwarding the abort signal into whatever it calls, as shown under [Writing a custom source](#writing-a-custom-source) below. A source that resolves from local data can ignore the signal; nothing else is required of it.

## Changing what a source queries

The constructor's second argument takes the query parameters alongside the source options, so both can be set in one place:

```tsx
const searchSource = useMemo(
  () =>
    new UserSearchSource(client, {
      filters: { id: { $ne: client.userID }, role: { $eq: "admin" } },
      pageSize: 30,
      sort: [{ name: 1 }],
    }),
  [client],
);
```

`MessageSearchSource` names its query parameters after the request each one shapes: `messageSearchChannelFilters`, `messageSearchFilters`, `messageSearchSort`, `channelQueryFilters`, `channelQuerySort` and `channelQueryOptions`.

Each one is also a property on the instance, which is how you change it after construction, for example from a filter control:

```tsx
searchSource.filters = { ...searchSource.filters, role: { $eq: "moderator" } };
```

## Writing a custom source

Extend `BaseSearchSource<T>` when you need to query an entity the SDK does not cover. A custom source needs a stable `type`, a `query()` implementation returning `{ items }`, and optionally `filterQueryResults()` to reshape a page before the list sees it.

```tsx
import { BaseSearchSource } from "stream-chat";
import type { SearchQueryOptions, SearchSourceOptions } from "stream-chat";

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

class ProjectSearchSource extends BaseSearchSource<Project> {
  readonly type = "projects";

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

  // forward the abort signal so a superseded query stops in flight
  protected async query(
    searchQuery: string,
    queryOptions?: SearchQueryOptions,
  ) {
    const response = await fetch(
      `https://example.com/api/projects?q=${encodeURIComponent(searchQuery)}`,
      { signal: queryOptions?.signal },
    );
    return { items: (await response.json()) as Project[] };
  }

  protected filterQueryResults(items: Project[]) {
    return items;
  }
}
```

The source skips its state update for an aborted query, so an abort rejection is never surfaced through `lastQueryError`. Avoid translating it into an empty result set or an error message of your own.


---

This page was last updated at 2026-08-14T17:46:56.970Z.

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