Rendered from the source repository. Headings, examples, code, tables, links, and referenced images are preserved.
assistant-ui React Native
Always consult [assistant-ui.com/llms.txt](https://www.assistant-ui.com/llms.txt) for the latest API.
@assistant-ui/react-native supplies runtime-connected, unstyled React Native primitives. Use them with View, Text, Pressable, TextInput, FlatList, and native styles to build the chat surface. @assistant-ui/ai-sdk supplies the AI SDK v7 runtime and transport. The model route runs in a separate backend project, never inside the Expo bundle.
Contents
- References | Quick start | Manual setup | Native chat composition | Generative toolkits | Common Gotchas | Related Skills
References
- ./references/primitives.md -- native primitive namespaces and their parts
- ./references/hooks.md -- state, runtime, tool, and scoped-provider hooks
- ./references/adapters.md -- local persistence, attachments, and remote thread lists
- ./references/custom-backend.md -- a streaming model adapter and a backend-owned thread list
- ./references/migration.md -- moving a web runtime to a native UI layer
Quick start
Start from the maintained Expo example:
npx assistant-ui@latest create --example with-expo my-app
cd my-appSet an endpoint that the app can reach. It must be an absolute URL. A physical device cannot resolve its own localhost to your development server.
EXPO_PUBLIC_CHAT_ENDPOINT_URL="https://api.example.com/api/chat"Start Expo:
npx expo startManual setup
Install the native runtime and its AI SDK v7 peer packages in an existing Expo app:
npx expo install @assistant-ui/react-native @assistant-ui/ai-sdk ai@^7 @ai-sdk/react@^4Host the model route separately. The native app posts UI messages to that route through AssistantChatTransport; the route converts them asynchronously for AI SDK v7 and returns a UI message stream.
import { openai } from "@ai-sdk/openai";
import { convertToModelMessages, streamText } from "ai";
export async function POST(request: Request) {
const { messages } = await request.json();
const result = streamText({
model: openai("gpt-5.6-luna"),
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}Create the runtime in a hook. Keep the endpoint in the public Expo environment so the compiled app can reach it.
import {
AssistantChatTransport,
useChatRuntime,
} from "@assistant-ui/ai-sdk";
const chatEndpoint = process.env.EXPO_PUBLIC_CHAT_ENDPOINT_URL;
export function useAppRuntime() {
if (!chatEndpoint) {
throw new Error("EXPO_PUBLIC_CHAT_ENDPOINT_URL is required");
}
return useChatRuntime({
transport: new AssistantChatTransport({ api: chatEndpoint }),
});
}AssistantChatTransport forwards frontend tool schemas and system messages. When the backend enables frontend tools, consume the request tools with frontendTools from @assistant-ui/ai-sdk; see tools for the shared backend contract.
Native chat composition
Put the runtime under the native AssistantRuntimeProvider, then compose the thread and composer from primitives. ThreadPrimitive.MessagesFlatList is the current list primitive and scopes each row to the corresponding message.
import {
AssistantRuntimeProvider,
AuiIf,
ComposerPrimitive,
MessagePrimitive,
ThreadPrimitive,
useAuiState,
} from "@assistant-ui/react-native";
import { Text, View } from "react-native";
import { useAppRuntime } from "./use-app-runtime";
function MessageRow() {
const role = useAuiState((s) => s.message.role);
return (
<View
style={{
alignSelf: role === "user" ? "flex-end" : "flex-start",
backgroundColor: role === "user" ? "#007aff" : "#f0f0f0",
borderRadius: 16,
margin: 8,
padding: 12,
}}
>
<MessagePrimitive.Content />
</View>
);
}
function Composer() {
return (
<ComposerPrimitive.Root style={{ flexDirection: "row", gap: 8, padding: 12 }}>
<ComposerPrimitive.Input
multiline
placeholder="Message..."
style={{ borderWidth: 1, borderRadius: 20, flex: 1, padding: 10 }}
/>
<ComposerPrimitive.Send>
<Text>Send</Text>
</ComposerPrimitive.Send>
</ComposerPrimitive.Root>
);
}
function ChatScreen() {
return (
<ThreadPrimitive.Root style={{ flex: 1 }}>
<AuiIf condition={(s) => s.thread.isEmpty}>
<Text style={{ padding: 16 }}>Send a message to begin.</Text>
</AuiIf>
<ThreadPrimitive.MessagesFlatList autoScroll>
{() => <MessageRow />}
</ThreadPrimitive.MessagesFlatList>
<Composer />
</ThreadPrimitive.Root>
);
}
export default function App() {
const runtime = useAppRuntime();
return (
<AssistantRuntimeProvider runtime={runtime}>
<ChatScreen />
</AssistantRuntimeProvider>
);
}MessagePrimitive.Content defaults text parts to native Text, not Markdown. Pass renderText or use MessagePrimitive.Parts with a React Native Markdown renderer when the model returns Markdown. For native keyboard handling, place the thread in a KeyboardAvoidingView and tune it for the platform.
Use useAuiState inside primitive scopes for reactive values. Use useAui() with no arguments for imperative actions such as aui.composer.send() and aui.thread.cancelRun(). Keep selectors to primitives or stable references instead of constructing an object or array in the selector.
Generative toolkits
Metro must compile files that start with "use generative". Install @assistant-ui/metro and wrap the default config. Expo gets getDefaultConfig from expo/metro-config; a bare React Native app gets it from @react-native/metro-config.
const { getDefaultConfig } = require("expo/metro-config");
const { withAui } = require("@assistant-ui/metro");
module.exports = withAui(getDefaultConfig(__dirname));For a backendless toolkit, pass the aui option through the wrapper so frontend and human tool schemas remain uploadable:
module.exports = withAui({
...getDefaultConfig(__dirname),
aui: { backendless: true },
});Write the toolkit against @assistant-ui/react-native, including native View and Text renderers. The "use generative" directive makes the compiler infer tool kind from execute. The tools skill covers backend, frontend, human, provider, external, and stub tool semantics.
Common Gotchas
The mobile app posts to /api/chat and never reaches the server
- Native apps have no browser origin for relative requests. Set
EXPO_PUBLIC_CHAT_ENDPOINT_URLto the complete route URL. - Use a host reachable from the simulator or physical device. Device localhost is the device itself.
The provider is mounted but primitives throw or show no state
- Create the runtime with
useChatRuntimeor another supported runtime hook, then pass it asruntime={runtime}toAssistantRuntimeProvider. - Render primitives below that provider. Message, part, attachment, queue, suggestion, and thread-list-item primitives also need their corresponding parent render scope.
A web Thread or an Elements import does not render in the Expo app
- The shadcn Elements catalog is DOM and Tailwind based. Build the native UI with
@assistant-ui/react-nativeprimitives and React Native styles.
The message list does not stay at the bottom
- Use
ThreadPrimitive.MessagesFlatListwithautoScrollfor new screens.ThreadPrimitive.Messagesis retained for compatibility and defaults its auto-scroll options to false.
Toolkits compile as ordinary modules or tool schemas never reach the model
- Add
withAuitoMetrobefore using"use generative". - Import the identical generative module in the server build. If there is no server build, set
aui: { backendless: true }.
Markdown appears as literal asterisks and fences
MessagePrimitive.Contentuses nativeTextby default. Supply a native Markdown renderer throughrenderText.
Related Skills
- setup --
create,initialize, and configure assistant-ui web projects - primitives -- DOM primitives for web applications, not React Native UI
- runtime -- runtime behavior and backend transport concepts shared with native
- tools -- generative toolkits, their backend route, and custom tool UI

