--- title: Expo React Native AI Chat description: Native iOS and Android AI chat app with Expo — drawer navigation, thread persistence, and the assistant-ui React Native components for mobile. --- ## Overview A full-featured React Native chat application built with Expo and `@assistant-ui/react-native`. It demonstrates drawer-based thread management, streaming OpenAI responses, and a polished native UI with dark mode support. ## Features - **Native UI**: Built with React Native primitives — no web views - **Drawer Navigation**: Swipeable sidebar for thread management - **Thread Management**: Create, switch, and browse conversations - **Streaming**: Real-time streaming via a server-side chat API endpoint - **Dark Mode**: Automatic light/dark theme support - **Keyboard Handling**: Proper `KeyboardAvoidingView` integration ## Quick Start ```bash # Clone the repo git clone https://github.com/assistant-ui/assistant-ui.git cd assistant-ui # Install dependencies pnpm install # Set your API key echo 'OPENAI_API_KEY="sk-..."' > examples/with-expo/.env # Run the example pnpm --filter with-expo start ``` ## Code ### Runtime Setup The runtime uses `useChatRuntime` from `@assistant-ui/react-ai-sdk` with an `AssistantChatTransport` that connects to a chat API endpoint: ```tsx title="hooks/use-app-runtime.ts" import { useMemo } from "react"; import { useChatRuntime, AssistantChatTransport, } from "@assistant-ui/react-ai-sdk"; const CHAT_API = process.env.EXPO_PUBLIC_CHAT_ENDPOINT_URL ?? "/api/chat"; export function useAppRuntime() { const transport = useMemo( () => new AssistantChatTransport({ api: CHAT_API }), [], ); return useChatRuntime({ transport }); } ``` ### App Layout `AssistantRuntimeProvider` wraps the Expo Router drawer layout inside a `GestureHandlerRootView`. The layout also handles font loading, theming, a "New Chat" button in the header, and tool registration via `useAui`: ```tsx title="app/_layout.tsx" import { DarkTheme, DefaultTheme, ThemeProvider, } from "@react-navigation/native"; import { Drawer } from "expo-router/drawer"; import { StatusBar } from "expo-status-bar"; import "react-native-reanimated"; import { Pressable, useColorScheme } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import { useFonts } from "expo-font"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { AssistantRuntimeProvider, useAui, Tools, } from "@assistant-ui/react-native"; import { useAppRuntime } from "@/hooks/use-app-runtime"; import { ThreadListDrawer } from "@/components/thread-list/ThreadListDrawer"; import { expoToolkit } from "@/components/assistant-ui/tools"; function NewChatButton() { const aui = useAui(); const colorScheme = useColorScheme(); const isDark = colorScheme === "dark"; return ( { aui.threads().switchToNewThread(); }} style={{ marginRight: 16 }} > ); } function DrawerLayout() { const colorScheme = useColorScheme(); return ( } screenOptions={{ headerRight: () => , drawerType: "front", swipeEnabled: true, drawerStyle: { backgroundColor: "transparent" }, }} > ); } export default function RootLayout() { const [fontsLoaded] = useFonts(Ionicons.font); const runtime = useAppRuntime(); const aui = useAui({ tools: Tools({ toolkit: expoToolkit }), }); if (!fontsLoaded) return null; return ( ); } ``` ```tsx title="app/index.tsx" import { Thread } from "@/components/assistant-ui/thread"; export default function ChatPage() { return ; } ``` ### Key Architecture | Layer | Purpose | |-------|---------| | `useChatRuntime` | Creates a runtime backed by an `AssistantChatTransport` connecting to a chat API endpoint | | `AssistantRuntimeProvider` | Provides the runtime to the entire app (thread and composer scopes are set up automatically) | | `useAuiState((s) => s.thread)` | Reactive thread state access with selector for fine-grained re-renders | | `useAuiState((s) => s.composer)` | Reactive composer state access | | Primitives | `ThreadMessages`, `ComposerInput`, `MessageContent`, etc. | ## Source