expo
Type
External
Status
Published
Created
Mar 17, 2026
Updated
Mar 17, 2026

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#

# 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:

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:

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>
  );
}
import { Thread } from "@/components/assistant-ui/thread";

export default function ChatPage() {
  return <Thread />;
}

Key Architecture#

LayerPurpose
useChatRuntimeCreates a runtime backed by an AssistantChatTransport connecting to a chat API endpoint
AssistantRuntimeProviderProvides 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
PrimitivesThreadMessages, ComposerInput, MessageContent, etc.

Source#