SDK Integration

The AI components integrate directly with the React Native SDK.

Best Practices

  • Keep AI-specific rendering gated by a stable isMessageAIGenerated check.
  • Use the SDK composer APIs to preserve uploads, drafts, and optimistic UI.
  • Stream AI state through a single channel source to avoid conflicting UI signals.
  • Render typing indicators only for active AI states to reduce noise.
  • Ensure stopGenerating is always available when AI is producing output.

StreamingMessageView

The SDK exposes an isMessageAIGenerated factory that tells contexts when to render a streaming view.

Example: treat message.ai_generated === true as AI-generated.

In that case, our factory function would look something like this:

const isMessageAIGenerated = (message: LocalMessage) => !!message.ai_generated;

Then pass your custom StreamingMessageView:

import { StreamingMessageView } from "@stream-io/chat-react-native-ai";

const CustomStreamingMessageView = () => {
  const { message } = useMessageContext();
  return (
    <View style={{ width: "100%", paddingHorizontal: 16 }}>
      <StreamingMessageView text={message.text ?? ""} />
    </View>
  );
};

// ...

<Channel
  {...otherChannelProps}
  StreamingMessageView={CustomStreamingMessageView}
/>;

This renders a full-width message using the AI StreamingMessageView instead of the default.

ComposerView

To use ComposerView, create a send resolver and use it instead of MessageInput:

import { ComposerView } from "@stream-io/chat-react-native-ai";

const CustomComposerView = () => {
  const messageComposer = useMessageComposer();
  const { sendMessage } = useMessageInputContext();
  const { channel } = useChannelContext();

  const { aiState } = useAIState(channel);

  const stopGenerating = useCallback(
    () => channel?.stopAIResponse(),
    [channel],
  );

  const isGenerating = [AIStates.Thinking, AIStates.Generating].includes(
    aiState,
  );

  const serializeToMessage = useStableCallback(
    async ({ text, attachments }: { text: string; attachments?: any[] }) => {
      messageComposer.textComposer.setText(text);
      if (attachments && attachments.length > 0) {
        const localAttachments = await Promise.all(
          attachments.map((a) =>
            messageComposer.attachmentManager.fileToLocalUploadAttachment(a),
          ),
        );
        messageComposer.attachmentManager.upsertAttachments(localAttachments);
      }

      await sendMessage();
    },
  );

  return (
    <ComposerView
      onSendMessage={serializeToMessage}
      isGenerating={isGenerating}
      stopGenerating={stopGenerating}
    />
  );
};

What this does:

  • Sets text and attachments on the composer state.
  • Calls sendMessage after the composer is updated.
  • Passes isGenerating and stopGenerating to control the stop button.

AITypingIndicatorView

You can render a thinking indicator below MessageList and above ComposerView:

import { AITypingIndicatorView } from "@stream-io/chat-react-native-ai";

const CustomAIThinkingIndicatorView = () => {
  const { channel } = useChannelContext();
  const { aiState } = useAIState(channel);

  const allowedStates = {
    [AIStates.Thinking]: "Thinking about the question...",
    [AIStates.Generating]: "Generating a response...",
    [AIStates.ExternalSources]: "Checking external sources...",
  };

  if (aiState === AIStates.Idle || aiState === AIStates.Error) {
    return null;
  }

  return (
    <View
      style={{
        paddingHorizontal: 24,
        paddingVertical: 12,
      }}
    >
      <AITypingIndicatorView text={allowedStates[aiState]} />
    </View>
  );
};

// ...

<Channel {...channelProps}>
  <MessageList />
  <CustomAIThinkingIndicatorView />
  <CustomComposerView />
</Channel>;