# Emoji 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/).

This UI cookbook turns on emoji suggestions in the composer, end to end. When the user types `:` and a few letters, a list suggests matching emoji, each with its shortcode, and picking one inserts it. The suggestions come from [emoji-mart](https://github.com/missive/emoji-mart)'s search index, with a few emoji of your app's own added in front. What the feature does and how the middleware works is explained in [Emoji](https://getstream.io/chat/docs/sdk/react-native/v10/features/rich-content/emoji/).

## Best practices

- Initialize emoji-mart once, at module scope, not inside a component.
- Create the emoji middleware inside the composer setup function, so each composer gets its own search source.
- Insert it before the mentions middleware, with `unique: true`, and remove it in the teardown.
- Keep all composer setup in one function. A client has one setup function per kind of object, so a second call replaces the first.
- Keep the search index fast. It runs on every keystroke after the trigger.
- Keep the `:` trigger unless you also replace the suggestion list. The SDK's list renders emoji rows for `:` only.

## Install emoji-mart

emoji-mart and its data are optional packages. Nothing in the SDK imports them:

**RN CLI**

```bash title="Terminal"
yarn add emoji-mart @emoji-mart/data
```

**Expo**

```bash title="Terminal"
npx expo install emoji-mart @emoji-mart/data
```

## Build the search index

`createTextComposerEmojiMiddleware` searches an `EmojiSearchIndex`: any object with a `search(query)` method that returns emoji, or a promise of them. emoji-mart's `SearchIndex` is one. This index puts the app's own emoji in front of emoji-mart's results:

```ts title="src/emojiSearchIndex.ts"
import data from "@emoji-mart/data";
import { init, SearchIndex } from "emoji-mart";
import type { Emoji, EmojiSearchIndex } from "stream-chat-react-native";

void init({ data });

const teamEmoji: Emoji[] = [
  { id: "shipit", name: "Ship it", native: "\u{1F680}", skins: [] },
  { id: "lgtm", name: "Looks good to me", native: "\u{2705}", skins: [] },
];

export const emojiSearchIndex: EmojiSearchIndex = {
  search: async (query) => {
    const lowerCaseQuery = query.toLowerCase();
    const ownMatches = teamEmoji.filter(
      ({ id, name }) =>
        id.includes(lowerCaseQuery) ||
        name.toLowerCase().includes(lowerCaseQuery),
    );
    const emojiMartMatches: Emoji[] = (await SearchIndex.search(query)) ?? [];

    return [...ownMatches, ...emojiMartMatches];
  },
};
```

An emoji has an `id`, which is its shortcode, a `name` and `skins`, a list of `{ native }`, and optionally `native`, `emoticons` and `keywords`. The middleware inserts `native`, or the first skin's `native` when it has none, and leaves out an emoji with neither. It shows the first seven results, so the app's emoji come first.

Emoticons go through the same index. When the word before the cursor follows the trigger, such as `:)`, the middleware searches the index for that word, and replaces it with the first of the seven results whose `emoticons` list it. emoji-mart's index finds emoji by their emoticons. The app's emoji above have none, so they never replace one.

## Install the middleware

Insert the middleware in the composer setup function. This one also installs the command middleware from the [Commands UI cookbook](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/composer/commands-ui/), because both share the client's one composer setup function:

```ts title="src/setUpComposers.ts"
import type { StreamChat, TextComposerMiddleware } from "stream-chat";
import {
  createTextComposerEmojiMiddleware,
  setupCommandUIMiddlewares,
} from "stream-chat-react-native";
import { emojiSearchIndex } from "./emojiSearchIndex";

export const setUpComposers = (client: StreamChat) =>
  client.config.setSetupFunction("messageComposer", ({ composer }) => {
    const emojiMiddleware = createTextComposerEmojiMiddleware({
      emojiSearchIndex,
    }) as TextComposerMiddleware;

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

    return () => {
      composer.textComposer.middlewareExecutor.remove([
        emojiMiddleware.id,
        "stream-io/text-composer/active-command-guard",
        "stream-io/text-composer/command-string-extraction",
      ]);
      composer.compositionMiddlewareExecutor.remove(
        "stream-io/message-composer-middleware/command-string-injection",
      );
      composer.draftCompositionMiddlewareExecutor.remove(
        "stream-io/message-composer-middleware/draft-command-string-injection",
      );
    };
  });
```

Call `setUpComposers(client)` once the client exists, before any `Channel` renders, as the [Commands UI cookbook](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/composer/commands-ui/#install-the-command-middleware) does. The setup function runs for the composers that exist and every one created later, so emoji suggestions work in channels, threads and while editing.

`createTextComposerEmojiMiddleware` returns a middleware typed for emoji suggestions, so it's cast to `TextComposerMiddleware` to go into the text composer's chain. Its id is `stream-io/react-native-sdk/emoji-middleware`. Inserting it before the mentions middleware runs it first, and while its list is open it completes the chain, so the mentions and commands middleware don't run for that change.

The function also takes `options`: `trigger`, `:` by default, and `minChars`, the shortest `:query` that opens suggestions, counting the colon, 1 by default. `minChars` also applies to emoticons, so `minChars: 3` stops `:)` from being replaced. The options are merged into defaults that the module shares, so pass the same options on every call.

## Show the shortcode in each row

The default emoji row shows the emoji and its name. Each suggestion also carries `tokenizedDisplayName`, its `id` split around the typed text, which `TokenizedSuggestionParts` renders with the typed part in bold. This row shows the emoji and `:shortcode:`. Emoji rows have no override key of their own, so it replaces `AutoCompleteSuggestionItem`, presses included, and keeps the default for the other triggers:

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

type TokenizedEmoji = EmojiSuggestion & Partial<TokenizationPayload>;

const isEmoji = (item: TextComposerSuggestion): item is TokenizedEmoji =>
  "native" in item && "name" in item;

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

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

  return (
    <Pressable
      accessibilityLabel={itemProps.name}
      accessibilityRole="button"
      onPress={() => void textComposer.handleSelect(itemProps)}
      style={styles.row}
    >
      <Text style={styles.emoji}>{itemProps.native}</Text>
      <Text style={{ color: semantics.textSecondary }}>:</Text>
      <TokenizedSuggestionParts
        fallback={itemProps.id}
        style={{ color: semantics.textSecondary }}
        tokenizedDisplayName={itemProps.tokenizedDisplayName}
      />
      <Text style={{ color: semantics.textSecondary }}>:</Text>
    </Pressable>
  );
};

const styles = StyleSheet.create({
  emoji: { fontSize: 22, marginRight: 8 },
  row: {
    alignItems: "center",
    flexDirection: "row",
    paddingHorizontal: 16,
    paddingVertical: 8,
  },
});
```

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

![Emoji suggestions with each emoji followed by its shortcode on iOS](https://getstream.io/docs-assets/images/064e7eb7ad46.png)

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

![Emoji suggestions with each emoji followed by its shortcode on Android](https://getstream.io/docs-assets/images/21bdc7b96155.png)

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

Register it under the `AutoCompleteSuggestionItem` key, declared once:

```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 { EmojiShortcodeItem } from "./EmojiShortcodeItem";

const overrides: ComponentOverrides = {
  AutoCompleteSuggestionItem: EmojiShortcodeItem,
};

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

`AutoCompleteSuggestionItem` renders the rows of every trigger. If you also replace the command rows, as the [Custom suggestions UI cookbook](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/composer/custom-suggestions/#change-the-rows) does, handle both triggers in one component. The header above the rows reads `Emoji matching "<query>"`, which isn't a translation key. Replace `AutoCompleteSuggestionHeader` to change it, as that UI cookbook shows.

## A trigger other than `:`

With `options: { trigger: "$" }`, the middleware opens suggestions after `$`, but the SDK's `AutoCompleteSuggestionList` renders nothing for a trigger other than `@`, `/` and `:`. Replace the list too, and render emoji rows when `suggestions.trigger` is your trigger, as [Rows for a trigger of your own](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/composer/custom-suggestions/#rows-for-a-trigger-of-your-own) does for `#`.

---

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