# Custom suggestions

> **Info:** This is release candidate documentation for **Stream Chat React Native SDK v10**. For the latest stable version, see the [latest version (v9)](https://getstream.io/chat/docs/sdk/react-native/).

The composer suggests items while the user types a trigger:

| Trigger | Suggests               | Example                                                |
| ------- | ---------------------- | ------------------------------------------------------ |
| `@`     | people and groups      | `@tom`, `@channel`, `@here`, `@admin`, `@Support Team` |
| `/`     | commands               | `/giphy`                                               |
| `:`     | emoji, once you add it | `:pizza`                                               |

This UI cookbook changes the suggestion list's header and rows, and adds a list for a trigger of your own. Where the suggestions come from, and how to add a trigger, is explained in [Suggestions](https://getstream.io/chat/docs/sdk/react-native/v10/state/writing-messages/suggestions/). Emoji suggestions need the emoji middleware: see the [Emoji suggestions UI cookbook](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/composer/emoji-suggestions/).

## Enabling mention types

User mentions always work. `@channel`, `@here`, roles and user groups appear only for users with the matching capability, and roles and user groups also need data on the server. [Mentions](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/mentions/#turning-it-on-or-off) lists the capabilities and where to grant them. The mentions middleware checks them itself, so a row you replace never shows a mention the user can't make.

## Best practices

- Replace the smallest part that does the job: `AutoCompleteSuggestionHeader` for the header, `MentionSuggestionItem` for the `@` rows, `AutoCompleteSuggestionItem` for every row, and `AutoCompleteSuggestionList` only for a trigger of your own or a list that looks different.
- Fall back to the SDK's component for the triggers and mention types you don't change, so they keep their behavior.
- Register the overrides with `WithComponents` around the screens they apply to. Channels and threads both read them.
- Pick an item with `TextComposer.handleSelect(item)`. It runs the trigger's middleware, which writes the item into the text and closes the list.
- Set `keyboardShouldPersistTaps="always"` on a list of your own, so the first tap on a row picks it rather than closing the keyboard.
- Keep rows light. The list renders again as the user types.

## What the list renders

`MessageList` and `MessageFlashList` render `AutoCompleteSuggestionList` above the composer. It follows the text composer's `suggestions` and the open search source's items, and renders nothing while there are no items. It renders:

- `AutoCompleteSuggestionHeader` above the rows, with `queryText` and `triggerType`. By default, the commands header reads "Instant Commands", the emoji header `Emoji matching "<query>"`, and mentions have no header.
- `AutoCompleteSuggestionItem` for each row, with `itemProps`, the suggestion, and `triggerType`. It's a button that picks the item, around the row for the trigger: `MentionSuggestionItem` for `@`, `CommandSuggestionItem` for `/` and `EmojiSuggestionItem` for `:`.

`triggerType` is `"mention"`, `"command"` or `"emoji"`. The list knows these three triggers only, and renders nothing for any other, even when the trigger's middleware opened suggestions. See [Rows for a trigger of your own](#rows-for-a-trigger-of-your-own).

## Change the header

The default headers aren't translation keys. This header translates them with keys of your own, adds one for mentions, and keeps the default for emoji:

```tsx title="src/SuggestionHeader.tsx"
import { StyleSheet, Text } from "react-native";
import {
  asDynamicKey,
  AutoCompleteSuggestionHeader,
  useTheme,
  useTranslationContext,
} from "stream-chat-react-native";
import type { AutoCompleteSuggestionHeaderProps } from "stream-chat-react-native";

export const SuggestionHeader = (props: AutoCompleteSuggestionHeaderProps) => {
  const { t } = useTranslationContext();
  const {
    theme: { semantics },
  } = useTheme();

  const title =
    props.triggerType === "command"
      ? t(asDynamicKey("myApp.suggestions.commands.title"), "Commands")
      : props.triggerType === "mention"
        ? t(asDynamicKey("myApp.suggestions.mentions.title"), "Mention someone")
        : null;

  if (!title) return <AutoCompleteSuggestionHeader {...props} />;

  return (
    <Text
      accessibilityRole="header"
      style={[styles.title, { color: semantics.textSecondary }]}
    >
      {title}
    </Text>
  );
};

const styles = StyleSheet.create({
  title: { fontSize: 13, fontWeight: "600", padding: 12 },
});
```

<table>
  <tr>
  <td align='center' width='50%'>

![A suggestion list with the header Mention someone and the channel and here rows on iOS](https://getstream.io/docs-assets/images/ffa6b2c1811d.png)

  </td>
  <td align='center' width='50%'>

![A suggestion list with the header Mention someone and the channel and here rows on Android](https://getstream.io/docs-assets/images/9adf56ea896b.png)

  </td>
  </tr>
</table>

`asDynamicKey` marks a key that isn't in the SDK's catalog, and the second argument is the English copy. [Localization](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/localization/) explains app keys.

## Change the rows

`AutoCompleteSuggestionItem` renders every row, so a replacement handles the press too. This one renders commands on one line, as `/giphy [text]`, and keeps the default rows for mentions and emoji:

```tsx title="src/CompactCommandItem.tsx"
import { Pressable, StyleSheet, Text } from "react-native";
import type { CommandSuggestion, TextComposerSuggestion } from "stream-chat";
import {
  AutoCompleteSuggestionItem,
  useMessageComposer,
  useTheme,
} from "stream-chat-react-native";
import type { AutoCompleteSuggestionItemProps } from "stream-chat-react-native";

const isCommand = (item: TextComposerSuggestion): item is CommandSuggestion =>
  "name" in item && "args" in item;

export const CompactCommandItem = (props: AutoCompleteSuggestionItemProps) => {
  const { itemProps, triggerType } = props;
  const { textComposer } = useMessageComposer();
  const {
    theme: { semantics },
  } = useTheme();

  if (triggerType !== "command" || !isCommand(itemProps)) {
    return <AutoCompleteSuggestionItem {...props} />;
  }

  return (
    <Pressable
      accessibilityRole="button"
      onPress={() => void textComposer.handleSelect(itemProps)}
      style={styles.row}
    >
      <Text style={[styles.name, { color: semantics.textPrimary }]}>
        /{itemProps.name}
      </Text>
      <Text style={{ color: semantics.textTertiary }}>{itemProps.args}</Text>
    </Pressable>
  );
};

const styles = StyleSheet.create({
  name: { fontWeight: "600" },
  row: {
    flexDirection: "row",
    gap: 8,
    paddingHorizontal: 16,
    paddingVertical: 10,
  },
});
```

<table>
  <tr>
  <td align='center' width='50%'>

![A suggestion list with the header Commands and one-line command rows on iOS](https://getstream.io/docs-assets/images/2cc1edac8fbd.png)

  </td>
  <td align='center' width='50%'>

![A suggestion list with the header Commands and one-line command rows on Android](https://getstream.io/docs-assets/images/5fdf27a66ff0.png)

  </td>
  </tr>
</table>

`TextComposer.handleSelect()` sets the command, as picking it from the default row does. The default row also grays out a command that can't be used in this message, such as a moderation command in a reply that quotes a message. This row doesn't. Picking such a command shows a warning and doesn't set it, from this row or the default one. `MessageComposer.isCommandDisabled(command)` tells whether a command is disabled.

### Rows per mention type

`MentionSuggestionItem` renders the `@` rows inside the default button, so replacing it changes the row and keeps the press. It receives the suggestion as its props, and `mentionType` tells which kind it is:

| `mentionType`      | Default row            | Shows                                                |
| ------------------ | ---------------------- | ---------------------------------------------------- |
| `user`             | `MentionUserItem`      | The avatar and the name, with the typed part in bold |
| `channel` / `here` | `MentionBroadcastItem` | `@channel` or `@here` and a description              |
| `role`             | `MentionRoleItem`      | The role and `Notify all {{ role }} members`         |
| `user_group`       | `MentionUserGroupItem` | The group's name and description                     |

The SDK exports the parts the default rows are built from: `MentionItem`, the row layout with a `leading` slot; `EnhancedMentionIcon`, an icon in a circle; `EnhancedMentionContent`, a title and a subtitle; and `TokenizedSuggestionParts`, a name with the typed part in bold. This one changes the `@channel` and `@here` rows and keeps the others:

```tsx title="src/BroadcastMentionRow.tsx"
import type { MentionSuggestion } from "stream-chat";
import {
  EnhancedMentionContent,
  EnhancedMentionIcon,
  MentionItem,
  MentionSuggestionItem,
  useComponentsContext,
} from "stream-chat-react-native";

export const BroadcastMentionRow = (item: MentionSuggestion) => {
  const { icons } = useComponentsContext();

  if (item.mentionType !== "channel" && item.mentionType !== "here") {
    return <MentionSuggestionItem {...item} />;
  }

  return (
    <MentionItem leading={<EnhancedMentionIcon Icon={icons.Megaphone} />}>
      <EnhancedMentionContent
        subtitle={
          item.mentionType === "channel"
            ? "Everyone in this channel gets a notification"
            : "Everyone online right now gets a notification"
        }
        title={`@${item.mentionType}`}
      />
    </MentionItem>
  );
};
```

The parts take their styles from `messageComposer.suggestions.mention` in the theme, so the row above follows them too. `MentionItem` applies `container` and `column`, `EnhancedMentionIcon` applies `enhancedMentionIcon`, and `EnhancedMentionContent` applies `enhancedMentionContainer`, `enhancedMentionTitle` and `enhancedMentionSubtitle`. The theme also has `name`, `tag` and `avatarSize` under `mention`, but no row reads them. See [Theming](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/theming/).

## Rows for a trigger of your own

[Custom triggers](https://getstream.io/chat/docs/sdk/react-native/v10/state/writing-messages/suggestions/#custom-triggers) adds a middleware that suggests tags after `#`, each with the tag as its `id`. The default list renders nothing for `#`, so replace the list. This one renders the tags, and the default list for every other trigger:

```tsx title="src/SuggestionListWithTags.tsx"
import { FlatList, Pressable, StyleSheet, Text, View } from "react-native";
import type {
  SearchSourceState,
  TextComposerState,
  TextComposerSuggestion,
} from "stream-chat";
import {
  AutoCompleteSuggestionList,
  useMessageComposer,
  useStateStore,
  useTheme,
} from "stream-chat-react-native";

const suggestionsSelector = ({ suggestions }: TextComposerState) => ({
  suggestions,
});
const itemsSelector = ({
  items,
}: SearchSourceState<TextComposerSuggestion>) => ({
  items,
});

export const SuggestionListWithTags = () => {
  const { textComposer } = useMessageComposer();
  const { suggestions } = useStateStore(
    textComposer.state,
    suggestionsSelector,
  );
  const { items } =
    useStateStore(suggestions?.searchSource.state, itemsSelector) ?? {};
  const {
    theme: { semantics },
  } = useTheme();

  if (suggestions?.trigger !== "#") return <AutoCompleteSuggestionList />;
  if (!items?.length) return null;

  return (
    <View
      style={[
        styles.list,
        {
          backgroundColor: semantics.backgroundCoreElevation1,
          borderColor: semantics.borderCoreDefault,
        },
      ]}
    >
      <FlatList
        data={items}
        keyboardShouldPersistTaps="always"
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <Pressable
            accessibilityRole="button"
            onPress={() => void textComposer.handleSelect(item)}
            style={styles.row}
          >
            <Text style={{ color: semantics.textPrimary }}>#{item.id}</Text>
          </Pressable>
        )}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  list: { borderTopWidth: 1, maxHeight: 240 },
  row: { paddingHorizontal: 16, paddingVertical: 10 },
});
```

The list subscribes to two stores, as the default one does: the text composer's `state`, for which trigger is open, and the open search source's `state`, for its items. Both selectors are declared at module scope. Picking a tag runs the middleware's `onSuggestionItemSelect`, which writes `#design` and a space into the text.

## Register the overrides

Put the replacements in one object, declared once, and pass it to `WithComponents`:

```tsx title="src/ChannelScreen.tsx"
import type { Channel as StreamChannel } from "stream-chat";
import {
  Channel,
  MessageComposer,
  MessageList,
  WithComponents,
} from "stream-chat-react-native";
import type { ComponentOverrides } from "stream-chat-react-native";
import { BroadcastMentionRow } from "./BroadcastMentionRow";
import { CompactCommandItem } from "./CompactCommandItem";
import { SuggestionHeader } from "./SuggestionHeader";
import { SuggestionListWithTags } from "./SuggestionListWithTags";

const overrides: ComponentOverrides = {
  AutoCompleteSuggestionHeader: SuggestionHeader,
  AutoCompleteSuggestionItem: CompactCommandItem,
  AutoCompleteSuggestionList: SuggestionListWithTags,
  MentionSuggestionItem: BroadcastMentionRow,
};

export const ChannelScreen = ({ channel }: { channel: StreamChannel }) => (
  <WithComponents overrides={overrides}>
    <Channel channel={channel}>
      <MessageList />
      <MessageComposer />
    </Channel>
  </WithComponents>
);
```

The default list reads the header and the row from the overrides, so `SuggestionListWithTags` shows `SuggestionHeader` and `CompactCommandItem` for the built-in triggers. A `Thread` on its own screen needs the same `WithComponents` around it.

## Hide the suggestions

`AutoCompleteSuggestionList: () => null` hides the list for every trigger. The middleware still runs and still searches, so mention suggestions keep querying members as the user types. To stop a trigger, remove its middleware in the composer setup function instead, as [Hide commands in the UI](https://getstream.io/chat/docs/sdk/react-native/v10/features/rich-content/commands-and-giphy/#hide-commands-in-the-ui) does for commands. The mentions middleware's id is `stream-io/text-composer/mentions-middleware`.

## Custom composers

A composer layout of your own keeps the suggestions as long as it renders `InputView` or `AutoCompleteInput`, because the text input updates the text composer that opens them. The list itself renders in the message list, so a screen without `MessageList` or `MessageFlashList` shows none. The [Custom composer UI cookbook](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/composer/custom-composer/) builds such a layout.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/composer/custom-suggestions/](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/composer/custom-suggestions/).