Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

169 lines
4.9 KiB
Plaintext

---
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 (
<Pressable
onPress={() => {
aui.threads().switchToNewThread();
}}
style={{ marginRight: 16 }}
>
<Ionicons
name="create-outline"
size={24}
color={isDark ? "#ffffff" : "#000000"}
/>
</Pressable>
);
}
function DrawerLayout() {
const colorScheme = useColorScheme();
return (
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<Drawer
drawerContent={(props) => <ThreadListDrawer {...props} />}
screenOptions={{
headerRight: () => <NewChatButton />,
drawerType: "front",
swipeEnabled: true,
drawerStyle: { backgroundColor: "transparent" },
}}
>
<Drawer.Screen name="index" options={{ title: "Chat" }} />
</Drawer>
<StatusBar style="auto" />
</ThemeProvider>
);
}
export default function RootLayout() {
const [fontsLoaded] = useFonts(Ionicons.font);
const runtime = useAppRuntime();
const aui = useAui({
tools: Tools({ toolkit: expoToolkit }),
});
if (!fontsLoaded) return null;
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<AssistantRuntimeProvider runtime={runtime} aui={aui}>
<DrawerLayout />
</AssistantRuntimeProvider>
</GestureHandlerRootView>
);
}
```
```tsx title="app/index.tsx"
import { Thread } from "@/components/assistant-ui/thread";
export default function ChatPage() {
return <Thread />;
}
```
### 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
<SourceLink href="https://github.com/assistant-ui/assistant-ui/tree/main/examples/with-expo" />