The @chatbotkit/sdk client and the conversation hooks from @chatbotkit/react run in React Native and Expo apps. Streaming works through a fetch implementation that exposes the response body as a stream, such as expo/fetch. Hooks are imported from the @chatbotkit/react/headless entry point, which has no react-dom or DOM dependencies.
Installation
npx expo install @chatbotkit/sdk @chatbotkit/reactshell
Expo SDK 52 and later ship expo/fetch, which supports streaming response bodies. Bare React Native apps need a fetch implementation with streaming support instead of the built-in one.
Authentication
Never ship an API secret in a mobile app. Anything bundled in the app can be extracted. Instead, create the conversation and a short-lived session token on your backend, and send only the token to the app:
// backend, e.g. an API route import { ChatBotKit } from '@chatbotkit/sdk' const cbk = new ChatBotKit({ token: process.env.CHATBOTKIT_API_TOKEN }) export async function createChatSession(botId) { const { id: conversationId } = await cbk.conversation.create({ botId }) const { token, expiresAt } = await cbk.conversation.session.create( conversationId, { durationInSeconds: 3600 } ) return { conversationId, token, expiresAt } }javascript
The session token is scoped to that conversation. Request a new one from your backend when it expires.
Streaming with expo/fetch
The built-in React Native fetch does not expose response.body, so it cannot stream. Pass expo/fetch as the fetchFn option:
import { fetch } from 'expo/fetch' import { ConversationClient } from '@chatbotkit/sdk' const client = new ConversationClient({ token: sessionToken, fetchFn: fetch }) let text = '' for await (const { type, data } of client .complete(conversationId, { text: 'Hello' }) .stream()) { if (type === 'token') { text += data.token } }javascript
If the fetch implementation returns a response without a readable body, stream() throws a StreamUnsupportedError with code: 'STREAM_UNSUPPORTED' rather than returning an empty stream:
import { StreamUnsupportedError } from '@chatbotkit/sdk' try { for await (const item of client.complete(conversationId, { text }).stream()) { // ... } } catch (error) { if (error instanceof StreamUnsupportedError) { console.warn('Configure a streaming fetchFn such as expo/fetch') } throw error }javascript
Calls that do not stream, such as await client.complete(conversationId, { text }), also work with the built-in fetch.
Hooks
Import the conversation manager hooks and context from @chatbotkit/react/headless. This entry point exports useConversationManager, useConversationManagerRemote, useConversationManagerState, useConversationManagerStateReducer, ConversationManager and ConversationContext, and does not import react-dom, react-markdown or any DOM API.
import { useMemo } from 'react' import { Button, FlatList, Text, TextInput, View } from 'react-native' import { fetch } from 'expo/fetch' import { useConversationManager } from '@chatbotkit/react/headless' import { ConversationClient } from '@chatbotkit/sdk' export function Chat({ conversationId, sessionToken }) { const client = useMemo( () => new ConversationClient({ token: sessionToken, fetchFn: fetch }), [sessionToken] ) const { message, messages, text, setText, submit, error } = useConversationManager({ client, conversationId }) return ( <View> <FlatList data={message ? [...messages, message] : messages} keyExtractor={(item) => item.id} renderItem={({ item }) => <Text>{item.text}</Text>} /> {error ? <Text>{error.message}</Text> : null} <TextInput value={text} onChangeText={setText} /> <Button title="Send" onPress={() => submit()} /> </View> ) }javascript
The components exported from the package root (ChatInput, ChatMessage, ChatMessages, AutoTextarea, AutoScroller) render DOM elements and are for the web only. react-dom and react-markdown are optional peer dependencies, so a React Native app does not need to install them.
When useConversationManager is given a string endpoint instead of a client, use an absolute URL. There is no window.location in React Native to resolve a relative path against.
File Uploads
file.upload() and conversation.attachment.upload() accept a file reference with uri, name and type, as returned by expo-image-picker and expo-document-picker:
import * as ImagePicker from 'expo-image-picker' const result = await ImagePicker.launchImageLibraryAsync() if (!result.canceled) { const asset = result.assets[0] await cbk.file.upload(fileId, { data: { uri: asset.uri, name: asset.fileName, type: asset.mimeType }, type: asset.mimeType, }) }javascript
The SDK reads the file with fetch(uri) into a Blob and uploads it. ArrayBuffer and string data work as before. The credential used must be allowed to upload to the file; session tokens are issued for a single conversation, so create the file on your backend and use a credential issued for that purpose.
Known Limits
- Streaming needs a fetch with a readable response body, such as
expo/fetchon Expo SDK 52 or later. The built-in React Native fetch is not supported forstream(). - Reading streams needs
TextDecoder. If your JavaScript engine does not provide it, install a polyfill such asfast-text-encodingand import it at the entry of your app. Without it,stream()throws aStreamUnsupportedErrorthat namesTextDecoder. - The web UI components in
@chatbotkit/reactare not available in React Native. Build the UI with React Native components on top of the headless hooks. - Widget hooks (
useWidgetInstance*) anduseDOMQuerySelectorare web-only.