# AI hooks

> **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/).

These hooks build AI chat UI: what the assistant is doing, its reply typed out as it streams in, the AI composer's state, and voice input. `useAIState` and `useStreamingMessage` are exported from `stream-chat-react-native` and `stream-chat-expo`. The others are exported from `stream-chat-react-native/ai-components` and `stream-chat-expo/ai-components`, the entry point of the AI components.

How the AI states, the streamed reply and the AI components fit together is on [AI integrations overview](https://getstream.io/chat/docs/sdk/react-native/v10/features/ai/ai-integrations-overview/), and the components of the main entry point on [SDK integration](https://getstream.io/chat/docs/sdk/react-native/v10/features/ai/sdk-integration/).

Each entry gives the signature, then:

- **Returns:** what the hook gives back.
- **Reads:** the store or the context it reads.
- **Re-renders:** what makes the component render again.
- **Needs:** where the component must sit, and what the hook does outside that place.

## useAIState

Returns what the AI is doing in a channel.

```ts
useAIState(channel?: Channel): { aiState: AIState }
```

- **Returns:** `{ aiState }`, one of the `AIStates`: `AI_STATE_IDLE`, `AI_STATE_THINKING`, `AI_STATE_EXTERNAL_SOURCES`, `AI_STATE_GENERATING`, `AI_STATE_ERROR` or `AI_STATE_STOP`, or a state of your own that your backend sends. `AI_STATE_IDLE` without a channel.
- **Reads:** `channel.state`, field `aiState`. The client sets it from the `ai_indicator.update`, `ai_indicator.clear` and `ai_indicator.stop` events, and resets it to idle when the connection closes or drops.
- **Re-renders:** when `aiState` changes, or the hook gets another channel.
- **Needs:** nothing. It doesn't read the channel from context: pass it, for example from `useChannel()`.

`AIStates` is exported from `stream-chat`, and also from `stream-chat-react-native`.

```tsx
import { Text } from "react-native";
import { AIStates } from "stream-chat";
import { useAIState, useChannel } from "stream-chat-react-native";

export const ThinkingIndicator = () => {
  const channel = useChannel();
  const { aiState } = useAIState(channel);

  if (aiState !== AIStates.Thinking) return null;
  return <Text>The assistant is thinking...</Text>;
};
```

Render it inside `Channel`. `AITypingIndicatorView` and the composer's stop button read the same state: while it's `AI_STATE_THINKING` or `AI_STATE_GENERATING`, the composer shows `StopMessageStreamingButton` in place of the send button, which calls `Channel.stopAIResponse()`.

## useStreamingMessage

Types a text out a few letters at a time, the way an AI reply appears as it streams in.

```ts
useStreamingMessage(params: {
  text: string;
  letterInterval?: number;
  renderingLetterCount?: number;
}): { streamedMessageText: string }
```

The params:

- `text`: the full text so far, such as the message's `text`. Required.
- `letterInterval`: the time between two steps, in milliseconds. `0` by default, which steps as fast as the JavaScript thread allows.
- `renderingLetterCount`: the letters added at each step, `2` by default.

- **Returns:** `{ streamedMessageText }`, the part of `text` typed out so far. While the part has an odd number of code fences (three backticks), a closing fence is added, so a code block renders as one while it streams.
- **Reads:** no store and no context. It runs its own timer.
- **Re-renders:** at each step until it reaches the end of `text`.
- **Needs:** nothing.

How the typing follows `text`:

1. The text there is when the component mounts shows at once. Only text added later is typed out.
2. When `text` grows, typing goes on from where it was.
3. When `text` gets shorter than what's on screen, the new text shows at once at the next step.

`StreamingMessageView` renders the text of AI messages with it, the ones `Chat`'s `isMessageAIGenerated` marks. To change only the speed, give the default component other values. To render the text your own way, call the hook in a `StreamingMessageView` of your own:

```tsx
import {
  MessageTextContainer,
  useMessageContext,
  useStreamingMessage,
} from "stream-chat-react-native";
import type { StreamingMessageViewProps } from "stream-chat-react-native";

export const SlowStreamingMessageView = ({
  message: messageFromProps,
}: StreamingMessageViewProps) => {
  const { message: messageFromContext } = useMessageContext();
  const message = messageFromProps ?? messageFromContext;
  const { streamedMessageText } = useStreamingMessage({
    letterInterval: 30,
    renderingLetterCount: 2,
    text: message.text ?? "",
  });

  return (
    <MessageTextContainer message={{ ...message, text: streamedMessageText }} />
  );
};
```

Pass it to `WithComponents` as `StreamingMessageView`, as [Overriding components](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/overriding-components/) describes. `stream-chat-react-native/ai-components` exports its own `useStreamingMessage`, which behaves the same and backs its markdown `StreamingMessageView`.

## useDictation

Turns speech into text with the platform's speech recognition. Import it from `stream-chat-react-native/ai-components`.

```ts
useDictation(defaultOptions?: DictationStartOptions): {
  cancel: () => void;
  start: (override?: DictationStartOptions) => Promise<void>;
  stop: () => void;
  toggle: () => void;
}
```

`DictationStartOptions` takes `language`, `interimResults` and `silenceTimeoutMs`. Without `defaultOptions`, the hook uses `{ language: 'en-US', interimResults: true, silenceTimeoutMs: 2500 }`. Options you pass replace these as a whole, so name every field you need.

- **Returns:**
  - `start(override?)`: starts recognition with the default options merged with `override`. On Android it asks for the `RECORD_AUDIO` permission first, and when it's refused it sets the error `NO_PERMISSION` and doesn't start. It clears the previous transcript and error.
  - `stop()`: stops listening. The recognizer moves to `stopping` and delivers the result of what it heard.
  - `cancel()`: stops listening at once, and the recognizer goes back to `idle`.
  - `toggle()`: stops while recording, starts otherwise.
- **Reads:** the native `AIDictation` module's events, which it writes into the dictation store: each result as `transcript`, each state change as `state` and `isRecording`, and each error as `error`. A final result and an error also set `state` to `'idle'` and `isRecording` to `false`. The store is shared by the whole app.
- **Re-renders:** when recording starts or stops.
- **Needs:** nothing. The native module must be in the build: always on Android, and on iOS only when the app opts in, as [SDK integration](https://getstream.io/chat/docs/sdk/react-native/v10/features/ai/sdk-integration/) explains. Without it, `start()` does nothing. `isDictationAvailable()` returns whether the module is there.

The SDK doesn't declare `RECORD_AUDIO`: add it to your app's `AndroidManifest.xml`. `SpeechToTextButton` is built on this hook, and cancels recognition when it unmounts.

## The dictation store

`useDictation` writes what the recognizer reports into `dictationStore`, a store exported from `stream-chat-react-native/ai-components`. Read it with `useStateStore` and a selector of your own:

| Field         | What it holds                                                    |
| ------------- | ---------------------------------------------------------------- |
| `state`       | `'idle'`, `'starting'`, `'listening'` or `'stopping'`            |
| `isRecording` | whether the recognizer is starting or listening                  |
| `transcript`  | the text recognized so far. It changes with each interim result. |
| `error`       | the last error, `{ code, message }`, or `null`                   |

The store is shared by the whole app, and filled only while a component that calls `useDictation` is mounted. `dictationStoreApi.reset()` puts it back to its first state.

A dictation button for the default composer of the main entry point writes the transcript into the composer's text:

```tsx
import { useEffect, useRef } from "react";
import { Pressable, Text } from "react-native";
import type { DictationStoreState } from "stream-chat-react-native/ai-components";
import {
  dictationStore,
  isDictationAvailable,
  useDictation,
} from "stream-chat-react-native/ai-components";
import { useMessageComposer, useStateStore } from "stream-chat-react-native";

const dictationSelector = ({
  isRecording,
  transcript,
}: DictationStoreState) => ({
  isRecording,
  transcript,
});

export const DictationButton = () => {
  const messageComposer = useMessageComposer();
  const { cancel, toggle } = useDictation();
  const { isRecording, transcript } = useStateStore(
    dictationStore,
    dictationSelector,
  );

  const previousTranscript = useRef(transcript);

  useEffect(() => {
    if (transcript === previousTranscript.current) return;
    previousTranscript.current = transcript;
    if (transcript) {
      messageComposer.textComposer.setText(transcript);
    }
  }, [messageComposer, transcript]);

  useEffect(() => () => cancel(), [cancel]);

  if (!isDictationAvailable()) return null;

  return (
    <Pressable
      accessibilityRole="button"
      accessibilityState={{ selected: isRecording }}
      onPress={toggle}
    >
      <Text>{isRecording ? "Stop dictation" : "Dictate"}</Text>
    </Pressable>
  );
};
```

## AI composer hooks

`ComposerView`, the AI composer, keeps its own state, separate from `MessageComposer` of `stream-chat`. These hooks read it. Import them from `stream-chat-react-native/ai-components`.

| Hook                          | Returns                                                                                                                                    | Needs                              |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------- |
| `useMessageComposerContext()` | `{ state, setText, sendMessage, mediaPickerService }`: the composer's store, a setter for its text, the send function and the media picker | `ComposerView`. Throws outside it. |
| `useComposerText()`           | `{ text }` from the composer's store                                                                                                       | `ComposerView`. Throws outside it. |
| `useComposerHasText()`        | `{ hasText }`, whether the text isn't empty                                                                                                | `ComposerView`. Throws outside it. |
| `useBottomSheetState()`       | `{ open, height }` of the AI composer's bottom sheet                                                                                       | nothing                            |
| `useTheme()`                  | `{ theme }`, the AI components' theme                                                                                                      | `StreamTheme`. Throws outside it.  |

`sendMessage()` passes `{ text, attachments, custom }` to the `onSendMessage` prop of `ComposerView` and clears the composer. The media the user picked is in `mediaPickerService.state`, field `assets`: read it with `useStateStore`. Each hook re-renders when the values it returns change.

The AI components' `useMessageComposerContext` and `useTheme` are not the hooks of the same names in the main entry point: the main entry point's `useTheme` reads the chat theme, and it exports no `useMessageComposerContext`.

## Best practices

- Pass the channel to `useAIState`. It doesn't read one from context.
- Type out only the messages `isMessageAIGenerated` marks, as `MessageContent` does.
- Give a component that types out text a new `key` when it shows another message. Otherwise the hook goes on from the length of the old text.
- Check `isDictationAvailable()` before you show a dictation control.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v10/reference/hooks/ai-hooks/](https://getstream.io/chat/docs/sdk/react-native/v10/reference/hooks/ai-hooks/).