
Assistant-UI React Native (Expo): how do I reuse the same tools/runtime from my web app?
Most teams building with Assistant-UI start on the web and then ask how to bring the exact same tools, runtime, and agent logic into a React Native (Expo) app without duplicating everything. The good news is that Assistant-UI is just a React-based UI layer, so as long as your agent logic is separated from your frontend, you can reuse almost all of it in mobile with minimal changes.
Below is a practical, step‑by‑step approach to reusing your web app’s tools/runtime inside an Expo app, plus some patterns that make this reuse clean and maintainable.
1. Separate “agent logic” from “UI” in your web app
If your existing Assistant-UI web integration mixes React components directly with tools and runtime logic, the first step is to extract the agent layer into a framework‑agnostic module.
Aim for a structure like:
apps/
web/
src/
ui/ # Web UI only
main.tsx
mobile/
app/ # React Native / Expo UI only
packages/
ai-runtime/
src/
createRuntime.ts
tools/
searchTool.ts
codeInterpreter.ts
index.ts
Key ideas:
packages/ai-runtimecontains:- Tool definitions (e.g., LangChain/LangGraph tools, custom tools).
- Your runtime factory (
createRuntime,createAgent, etc.). - Any shared types or interfaces.
- Both web and mobile import from this shared package.
- Assistant-UI components in web/mobile only care about:
onSubmit/runAssistant- A
runtime/clientobject, or - A handler that streams responses from your backend.
If you already have this separation on the web, you’re halfway there.
2. Make your tools/runtime platform-agnostic
To reuse tools/runtime between web and React Native, avoid browser‑specific or Node‑specific APIs in your shared layer unless you polyfill them.
Typical fixes:
- Avoid direct DOM access (
document,window) in tools. - Use
fetchinstead ofaxiosif you don’t want to configure React Native networking separately. - If you use Node APIs (e.g.,
fs,crypto), move them:- Either into your backend (server routes / serverless functions).
- Or into platform-specific wrappers that your tools call indirectly.
A simple shared runtime factory might look like:
// packages/ai-runtime/src/createRuntime.ts
import { OpenAI } from "openai";
import { createTools } from "./tools";
export function createRuntime(options?: { apiKey?: string }) {
const client = new OpenAI({
apiKey: options?.apiKey ?? process.env.OPENAI_API_KEY,
});
const tools = createTools({ client });
return {
client,
tools,
};
}
Your tools live in ./tools and are just functions that don’t know (or care) if they are used from web or mobile.
3. Expose a single “assistant handler” for UI layers
To keep the UI integration identical between web and mobile, create a high‑level function that your UI can call:
// packages/ai-runtime/src/index.ts
import { createRuntime } from "./createRuntime";
const runtime = createRuntime();
export async function runAssistant({
threadId,
messages,
onToken,
}: {
threadId: string;
messages: Array<{ role: "user" | "assistant" | "system"; content: string }>;
onToken?: (token: string) => void;
}) {
// Example shape – adapt to your provider and tools
const { client, tools } = runtime;
const stream = await client.responses.stream({
model: "gpt-4.1-mini",
input: messages,
tools,
metadata: { threadId },
});
for await (const chunk of stream) {
onToken?.(chunk.output_text ?? "");
}
return stream.output();
}
Now both your web and Expo UI can call runAssistant in exactly the same way.
4. Use Assistant-UI on the web with the shared runtime
In your web app you might already have something like:
// apps/web/src/ui/Chat.tsx
import { Chat } from "assistant-ui"; // or similar
import { runAssistant } from "ai-runtime";
export function WebChat() {
return (
<Chat
// Pseudocode – adapt to the actual Assistant-UI props
onSubmit={async ({ messages, threadId, append }) => {
await runAssistant({
threadId,
messages,
onToken: (token) => append(token),
});
}}
/>
);
}
The important part: the UI doesn’t know about tools or OpenAI directly; it just calls runAssistant.
5. Add Assistant-UI to React Native (Expo)
In Expo, you’ll use the React‑compatible Assistant-UI chat components. The exact import path depends on your setup, but the pattern is similar: you render a chat component and hook it up to the same runAssistant function.
Example with React Native:
// apps/mobile/app/ChatScreen.tsx
import React from "react";
import { SafeAreaView } from "react-native";
import { Chat } from "assistant-ui"; // RN-compatible build
import { runAssistant } from "ai-runtime";
export default function ChatScreen() {
return (
<SafeAreaView style={{ flex: 1 }}>
<Chat
onSubmit={async ({ messages, threadId, append }) => {
await runAssistant({
threadId,
messages,
onToken: (token) => append(token),
});
}}
/>
</SafeAreaView>
);
}
Because runAssistant and your tools live in the shared ai-runtime package, you’re reusing the same runtime between web and mobile.
If your current stack uses LangChain, LangGraph, or the Vercel AI SDK, the pattern is identical: the mobile UI just calls the same handler function you already created for the web UI.
6. Decide: direct model calls vs backend endpoint
For React Native, you have two options:
Option A: Call the model directly from the device
- Put the client and tools directly in your shared package.
- Use the same code on web and mobile.
- Works best when:
- You’re fine with API keys on-device (using secure storage or per-user keys), or
- You use a proxy endpoint that issues temporary keys.
This is closest to your existing web approach if the web is also calling the model from the frontend.
Option B: Call a backend route from both web and mobile
- Keep tools and runtime on the server (Node / edge functions).
runAssistantbecomes a thin client helper that callsPOST /api/assistant.- Assistant-UI on web and mobile both call that same endpoint.
Example shared client:
// packages/ai-runtime/src/client.ts
export async function runAssistantViaApi({
threadId,
messages,
onToken,
}: {
threadId: string;
messages: any[];
onToken?: (token: string) => void;
}) {
const response = await fetch("https://your-api.com/api/assistant", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ threadId, messages }),
});
// If you support streaming, parse the stream here and call onToken
// Otherwise, just return JSON:
const result = await response.json();
return result;
}
Your web and mobile UI then both call runAssistantViaApi instead of a local runtime. This keeps all tools and secret keys in one central place.
7. Handling streaming and state management in Expo
Assistant-UI offers production-ready state management for AI chat: streaming, interruptions, retries, and multi-turn conversations. On mobile, this works the same way:
- Use the Assistant-UI chat component’s props to handle streaming.
- Provide a handler (
onSubmit,onStream, etc.) that uses:runAssistant(direct model access), orrunAssistantViaApi(backend streaming).
Because the state management is handled by Assistant-UI, your mobile app gets:
- Multi-turn conversation context.
- Streaming tokens.
- Consistent behavior with your web app.
You just need to ensure your runtime exposes a streaming interface that the UI can consume (as shown above with onToken callbacks).
8. Sharing types and tool definitions
To truly reuse tools and runtime, centralize the following in the shared package:
- Tool schemas (e.g., Zod schemas, function signatures).
- Type definitions for messages, threads, tool calls.
- Identifiers for tools (e.g.,
"search","code_interpreter").
Example:
// packages/ai-runtime/src/tools/index.ts
export type ToolContext = {
userId: string;
// any shared context like db clients, etc.
};
export function createTools(ctx: ToolContext) {
return {
search: async (query: string) => {
// implementation shared across web/mobile/backend
},
summarize: async (text: string) => {
// ...
},
};
}
export type Tools = ReturnType<typeof createTools>;
If you use LangGraph or LangChain, define your graph/chain in the shared package and only pass environment-specific dependencies (like DB clients) from your web/mobile or backend entry points.
9. Platform-specific extras (optional)
Sometimes tools need platform-specific capabilities (e.g., camera, GPS, local file access). For those:
- Define an abstract tool in the shared package.
- Implement platform-specific functions that you inject at runtime.
Example:
// shared
export type CaptureImageFn = () => Promise<string>; // returns base64 or URL
export function createTools({
captureImage,
}: {
captureImage: CaptureImageFn;
}) {
return {
capture_image: async () => {
return await captureImage();
},
// other tools
};
}
Then:
- On web:
captureImagemight open a file picker. - On Expo:
captureImagemight useexpo-image-picker.
This keeps the core runtime shared, while native capabilities are expressed via dependency injection.
10. Quick checklist for reusing web tools/runtime in Expo
To summarize the migration pattern for Assistant-UI React Native (Expo):
- Extract agent logic (runtime, tools, model client) into a shared module/package.
- Remove platform-specific code from that shared layer; inject it where needed.
- Expose a single handler (
runAssistantor a similar function) for your UI layers. - Use that handler in both:
- Web Assistant-UI components
- React Native (Expo) Assistant-UI components
- Decide whether your runtime:
- Runs directly in the client (web + mobile), or
- Lives on a backend API shared by both.
- Ensure streaming is exposed via a callback or async iterator so Assistant-UI can manage chat state consistently across platforms.
Following this pattern, your Expo app uses the exact same tools and runtime as your web app, while Assistant-UI handles the chat UI and state management for both environments.