AI hooks
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, and the components of the main entry point on 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.
useAIState(channel?: Channel): { aiState: AIState }- Returns:
{ aiState }, one of theAIStates:AI_STATE_IDLE,AI_STATE_THINKING,AI_STATE_EXTERNAL_SOURCES,AI_STATE_GENERATING,AI_STATE_ERRORorAI_STATE_STOP, or a state of your own that your backend sends.AI_STATE_IDLEwithout a channel. - Reads:
channel.state, fieldaiState. The client sets it from theai_indicator.update,ai_indicator.clearandai_indicator.stopevents, and resets it to idle when the connection closes or drops. - Re-renders: when
aiStatechanges, 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.
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.
useStreamingMessage(params: {
text: string;
letterInterval?: number;
renderingLetterCount?: number;
}): { streamedMessageText: string }The params:
-
text: the full text so far, such as the message'stext. Required. -
letterInterval: the time between two steps, in milliseconds.0by default, which steps as fast as the JavaScript thread allows. -
renderingLetterCount: the letters added at each step,2by default. -
Returns:
{ streamedMessageText }, the part oftexttyped 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:
- The text there is when the component mounts shows at once. Only text added later is typed out.
- When
textgrows, typing goes on from where it was. - When
textgets 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:
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 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.
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 withoverride. On Android it asks for theRECORD_AUDIOpermission first, and when it's refused it sets the errorNO_PERMISSIONand doesn't start. It clears the previous transcript and error.stop(): stops listening. The recognizer moves tostoppingand delivers the result of what it heard.cancel(): stops listening at once, and the recognizer goes back toidle.toggle(): stops while recording, starts otherwise.
- Reads: the native
AIDictationmodule's events, which it writes into the dictation store: each result astranscript, each state change asstateandisRecording, and each error aserror. A final result and an error also setstateto'idle'andisRecordingtofalse. 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 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:
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
isMessageAIGeneratedmarks, asMessageContentdoes. - Give a component that types out text a new
keywhen it shows another message. Otherwise the hook goes on from the length of the old text. - Check
isDictationAvailable()before you show a dictation control.