# Suggestions

Suggestions open while the user types a trigger: `@` lists people and groups to mention, and `/` lists commands. The text composer's middleware finds the trigger and the query typed after it, a search source loads the matching items, and picking an item writes it into the text.

>
> **Info:** **Example**
>
> The user types "Thanks @jo" in a channel:
>
> 1. `TextComposer.handleChange()` runs the `onChange` handlers of the text composer's middleware chain. The mentions middleware finds `@jo` just before the cursor.
> 2. It sets `textComposer.state.suggestions` to `{ trigger: "@", query: "jo", searchSource }`.
> 3. When the handlers are done, the composer calls `searchSource.search("jo")`. The search source keeps the results in its own `state`: `items`, `isLoading` and `hasNext`.
> 4. The user picks Cleo. `TextComposer.handleSelect(item)` runs the chain's `onSuggestionItemSelect` handlers: the text becomes "Thanks @Cleo ", Cleo is added to `textComposer.mentions`, and `suggestions` is cleared.
>

## How a suggestion list works

The text composer's middleware chain, `textComposer.middlewareExecutor`, does the work. Each method in the Executes column that has an event runs the handlers for that event in every middleware of the chain, in order. See [Composer middleware](https://getstream.io/chat/docs/sdk/react-native/state/writing-messages/composer-middleware/).

| Step                                                   | Executes                                            | Event                    | What happens                                                                                                                                                  |
| ------------------------------------------------------ | --------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1. The user types a trigger and a query, such as `@jo` | `TextComposer.handleChange()`                       | `onChange`               | The trigger's middleware finds `@jo` before the cursor, and sets `textComposer.suggestions` to `{ trigger: "@", query: "jo", searchSource }`: the list opens. |
| 2. The handlers have finished                          | `searchSource.search("jo")`, by the composer        | none                     | The search for "jo" starts, after a short pause. `searchSource.state` gets `isLoading`, then the first page in `items`, and `hasNext`.                        |
| 3. The user types more, such as `@joh`                 | `TextComposer.handleChange()`                       | `onChange`               | The middleware keeps its search source and sets the new query. A new search starts after a short pause, and `items` is replaced when the new results arrive.  |
| 4. The user scrolls to the end of the list             | `searchSource.search()` without a query, by your UI | none                     | The search for the next page starts, while `hasNext` is true. The page is added to `items`.                                                                   |
| 5a. The user picks an item                             | `TextComposer.handleSelect(item)`                   | `onSuggestionItemSelect` | The middleware writes the item into the text and clears `textComposer.suggestions`: the list closes.                                                          |
| 5b. The user dismisses the list                        | `TextComposer.closeSuggestions()`                   | none                     | `textComposer.suggestions` is cleared. The text stays as typed.                                                                                               |
| 5c. The user deletes the trigger                       | `TextComposer.handleChange()`                       | `onChange`               | The middleware doesn't find the trigger, and clears `textComposer.suggestions`.                                                                               |

A UI follows two stores: the text composer's `state` for whether a list is open, and the open list's `searchSource.state` for its items.

```ts
import type { TextComposer } from "stream-chat";

// Follows the open suggestion list and its items. Returns a function that stops both.
const followSuggestions = (textComposer: TextComposer) => {
  let stopFollowingItems: (() => void) | undefined;

  const stopFollowingList = textComposer.state.subscribeWithSelector(
    // Only the search source: it changes when a list opens, closes or gives way to
    // another trigger's list, not on every keystroke.
    ({ suggestions }) => ({ searchSource: suggestions?.searchSource }),
    ({ searchSource }) => {
      // Stop following the previous list's items, and follow the new list's, if one is open.
      stopFollowingItems?.();
      stopFollowingItems = searchSource?.state.subscribeWithSelector(
        ({ isLoading, items }) => ({ isLoading, items }),
        ({ isLoading, items }) => {
          console.log(
            isLoading ? "Loading..." : `${items?.length ?? 0} suggestions`,
          );
        },
      );
    },
  );

  return () => {
    stopFollowingList();
    stopFollowingItems?.();
  };
};
```

Call it with `channel.messageComposer.textComposer`, and call the function it returns to stop following.

The SDK's `AutoCompleteSuggestionList` follows the same two stores for the composer `useMessageComposer()` returns, and renders each item with `AutoCompleteSuggestionItem`. [Message composer UI](https://getstream.io/chat/docs/sdk/react-native/ui/components/message-composer-ui/) covers the components.

## Built-in triggers

| Trigger | Lists                                                        | Explained on                                                             |
| ------- | ------------------------------------------------------------ | ------------------------------------------------------------------------ |
| `@`     | users, `@channel`, `@here`, roles and user groups to mention | [Mentions](https://getstream.io/chat/docs/sdk/react-native/state/writing-messages/mentions/) |
| `/`     | the channel type's commands, at the start of the text        | [Commands](https://getstream.io/chat/docs/sdk/react-native/state/writing-messages/commands/) |
| `:`     | emoji, once the emoji middleware is installed                | [Emoji](https://getstream.io/chat/docs/sdk/react-native/features/rich-content/emoji/)        |

## When a list opens

`createMentionsMiddleware(channel, options)` and `createCommandsMiddleware(channel, options)` take two options that decide when their list opens:

- `minChars` is the number of characters, the trigger included, the user types before the list opens. It's `1` by default, so the trigger alone opens the list. With `minChars: 3`, the mentions list opens at `@jo`, and not at `@` or `@j`.
- `trigger` is the character that opens the list, `@` for mentions and `/` for commands by default. It changes only what opens the list. A picked mention is still inserted as `@` and its name, a picked command as `/` and its name, and a command typed in full is recognized only after `/`. The SDK's `AutoCompleteSuggestionList` renders the lists of `@`, `/` and `:` only, so a new trigger also needs list components of your own, as in [Custom triggers](#custom-triggers).

Replace the built-in middleware in the composer setup function. `MiddlewareExecutor.replace()` puts the new middleware in the place of the built-in one, because both have the same id:

```ts
import { createMentionsMiddleware, StreamChat } from "stream-chat";
import type { TextComposerMiddleware } from "stream-chat";

const client = StreamChat.getInstance("YOUR_API_KEY");

client.config.setSetupFunction("messageComposer", ({ composer }) => {
  const { channel, textComposer } = composer;

  // `@` and two more characters open the list.
  textComposer.middlewareExecutor.replace([
    createMentionsMiddleware(channel, {
      minChars: 3,
    }) as TextComposerMiddleware,
  ]);

  return () =>
    textComposer.middlewareExecutor.replace([
      createMentionsMiddleware(channel) as TextComposerMiddleware,
    ]);
});
```

Replace the commands middleware the same way, with `createCommandsMiddleware(channel, { composer, minChars })`. It needs the composer to refuse disabled commands, as [Replacing command search](https://getstream.io/chat/docs/sdk/react-native/state/writing-messages/commands/#replacing-command-search) explains.

## Custom triggers

A new trigger is a text middleware with two handlers: `onChange` opens the suggestions when the trigger is typed, and `onSuggestionItemSelect` writes the picked item into the text. `stream-chat` exports the helpers the built-in middleware use: `getTriggerCharWithToken()` finds the trigger and the query before the cursor, and `insertItemWithTrigger()` replaces them with the picked item.

This middleware suggests tags after `#`, from a fixed list:

```ts
import {
  BaseSearchSourceSync,
  getTriggerCharWithToken,
  insertItemWithTrigger,
  StreamChat,
} from "stream-chat";
import type {
  Middleware,
  SearchSourceType,
  TextComposerMiddleware,
  TextComposerMiddlewareExecutorState,
  TextComposerSuggestion,
} from "stream-chat";

const client = StreamChat.getInstance("YOUR_API_KEY");

type TagSuggestion = TextComposerSuggestion<{ name: string }>;

const TAGS = ["design", "release", "support"];

class TagSearchSource extends BaseSearchSourceSync<TagSuggestion> {
  readonly type: SearchSourceType = "tags";

  constructor() {
    super({ allowEmptySearchString: true });
  }

  protected query(searchQuery: string) {
    const items = TAGS.filter((tag) => tag.startsWith(searchQuery)).map(
      (tag) => ({ id: tag, name: tag }),
    );
    return { items, next: null };
  }

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

const createTagMiddleware = (): Middleware<
  TextComposerMiddlewareExecutorState<TagSuggestion>,
  "onChange" | "onSuggestionItemSelect"
> => {
  const trigger = "#";
  const searchSource = new TagSearchSource();
  searchSource.activate();

  return {
    id: "my-app/tags-middleware",
    handlers: {
      onChange: ({ complete, forward, next, state }) => {
        if (!state.selection) return forward();

        const triggerWithQuery = getTriggerCharWithToken({
          acceptTrailingSpaces: false,
          text: state.text.slice(0, state.selection.end),
          trigger,
        });

        if (!triggerWithQuery) {
          // Close the list this middleware opened, once the trigger is gone.
          if (state.suggestions?.trigger !== trigger) return forward();
          return next({ ...state, suggestions: undefined });
        }

        if (triggerWithQuery === trigger) searchSource.resetStateAndActivate();

        return complete({
          ...state,
          suggestions: {
            query: triggerWithQuery.slice(trigger.length),
            searchSource,
            trigger,
          },
        });
      },
      onSuggestionItemSelect: ({ complete, forward, state }) => {
        const selected = state.change?.selectedSuggestion;
        if (!selected || state.suggestions?.trigger !== trigger)
          return forward();

        return complete({
          ...state,
          ...insertItemWithTrigger({
            insertText: `#${selected.name} `,
            selection: state.selection,
            text: state.text,
            trigger,
          }),
          suggestions: undefined,
        });
      },
    },
  };
};

client.config.setSetupFunction("messageComposer", ({ composer }) => {
  const tags = createTagMiddleware() as unknown as TextComposerMiddleware;

  composer.textComposer.middlewareExecutor.insert({
    middleware: [tags],
    position: { before: "stream-io/text-composer/mentions-middleware" },
    unique: true,
  });

  return () => composer.textComposer.middlewareExecutor.remove(tags.id);
});
```

The middleware completes the chain while its list is open, so the mentions and commands middleware don't run for that change.

The SDK's `AutoCompleteSuggestionList` and `AutoCompleteSuggestionItem` render the `@`, `/` and `:` triggers only, and render nothing for a trigger they don't know. To show the items of a trigger of your own, replace both through `WithComponents`, as [Custom suggestions](https://getstream.io/chat/docs/sdk/react-native/ui-cookbooks/composer/custom-suggestions/) shows. The SDK's emoji middleware, `createTextComposerEmojiMiddleware()`, works the same way with `:` as its trigger. See [Emoji](https://getstream.io/chat/docs/sdk/react-native/features/rich-content/emoji/).

## Pitfalls

- **Two middleware with one trigger.** The first middleware in the chain that completes wins. Pick a trigger the built-in ones don't use, or put your middleware in place of theirs.
- **A search source per composer.** Create the search source inside the setup function, as the examples do, so composers don't share results.

## Used in

- [Mentions](https://getstream.io/chat/docs/sdk/react-native/features/messaging/mentions/)
- [Commands and Giphy](https://getstream.io/chat/docs/sdk/react-native/features/rich-content/commands-and-giphy/)
- [Emoji](https://getstream.io/chat/docs/sdk/react-native/features/rich-content/emoji/)
- [Custom suggestions](https://getstream.io/chat/docs/sdk/react-native/ui-cookbooks/composer/custom-suggestions/)

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/state/writing-messages/suggestions/](https://getstream.io/chat/docs/sdk/react-native/state/writing-messages/suggestions/).