Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions docs/content/docs/openui-lang/examples/react-native.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
---
title: React Native
description: Stream OpenUI Lang to a React Native (Expo) app using a Next.js backend.
---

A full-stack example that streams [OpenUI Lang](/docs/openui-lang/overview) into an Expo app using a Next.js backend, then renders it as native React Native UI with `<Renderer />` from `@openuidev/react-lang`.

[View source on GitHub →](https://github.com/thesysdev/crayon/tree/main/examples/openui-react-native)
Comment thread
abhithesys marked this conversation as resolved.

<div className="bg-[rgba(0,0,0,0.05)] dark:bg-gray-900 rounded-2xl p-2">
<video
src="/videos/react-native-demo.mp4"
noControls
playsInline
muted
preload="metadata"
className="h-[600px] rounded-lg m-auto"
autoPlay
loop
/>
</div>
The example includes two modules:

- `backend`: a Next.js backend that calls OpenAI and streams raw OpenUI Lang
- `chat-app`: an Expo app that progressively parses that stream and renders native components

## Architecture

```
Expo app -- POST /api/chat --> Next.js backend --> OpenAI
<-- text/plain stream -- (OpenUI Lang)
```

The backend loads a pre-generated `system-prompt.txt`, forwards the conversation to OpenAI with streaming enabled, and returns raw `text/plain` chunks. The mobile app accumulates those chunks and passes the growing string into `<Renderer />`, which progressively parses and renders native UI as the response arrives.

Using plain text keeps the transport simple. React Native does not expose the same browser streaming primitives you would normally use with `fetch()` plus `ReadableStream` or `EventSource`, so this example uses `XMLHttpRequest` and `onprogress` instead.

## Project layout

```
examples/openui-react-native/
|- backend/ # Next.js API that talks to OpenAI
\- chat-app/ # Expo app that renders streamed OpenUI Lang
```

## Run the example

Run these commands from `examples/openui-react-native`.

1. Install dependencies:

```bash
cd examples/openui-react-native
pnpm install
```

2. Configure the backend:

```bash
cp backend/env.example backend/.env.local
```

Then add your OpenAI key to `backend/.env.local`:

```bash
OPENAI_API_KEY=sk-...
```

3. Generate the prompt file used by the backend:

```bash
pnpm generate:prompt
```

This generates `backend/src/system-prompt.txt` from `backend/src/library.ts`. Re-run it any time you change the component names, schemas, descriptions, or prompt rules.

4. Start the Next.js backend:

```bash
pnpm dev:backend
```

5. Start the Expo app in a second terminal:

```bash
pnpm dev:mobile
```

By default, `chat-app/metro.config.js` auto-detects your local IP address and sets `EXPO_PUBLIC_BACKEND_URL` to `http://<your-ip>:3000/api/chat`. If you need to point the app somewhere else, set `EXPO_PUBLIC_BACKEND_URL` yourself before starting Expo.

If you are testing on a physical device, make sure the phone and your development machine are on the same network.

## What to expect

Open the Expo app and try one of the built-in prompt chips such as:

- "Top languages 2025"
- "Revenue trend"
- "Energy mix"

The assistant response should appear progressively. Instead of rendering markdown, the app parses the streamed OpenUI Lang and turns it into native `Text`, `BarChart`, `LineChart`, `PieChart`, and `Card` components.

## Request and render flow

1. The user sends a message from `chat-app/screens/ChatScreen.tsx`.
2. `chat-app/hooks/useStreamingChat.ts` posts the chat history to the backend with `XMLHttpRequest`.
3. `backend/src/app/api/chat/route.ts` forwards that history to OpenAI using the generated `system-prompt.txt`.
4. The backend streams raw OpenUI Lang text back to the phone.
5. `chat-app/store/streamStore.ts` keeps the accumulated response outside the main message list state.
6. `chat-app/components/BotBubble.tsx` subscribes to that stream and renders it with `<Renderer />`.

This separation is important: the message list only stores chat items, while the streamed OpenUI Lang lives in a dedicated store so each bot bubble can update independently.

## Key files

### `chat-app/library.tsx` - React Native renderers

Defines the React Native component library. It includes real renderers for `Text`, `BarChart`, `LineChart`, `PieChart`, and `Card`, using `react-native-svg` for the chart components.

This file must stay in sync with `backend/src/library.ts`: same component names, same prop schemas, same root component.

```tsx
export const library = createLibrary({
components: [
TextComponent,
BarChartComponent,
LineChartComponent,
PieChartComponent,
CardComponent,
],
root: "Card",
});
```

The mobile version contains the real React Native renderers. It is the code that actually turns parsed OpenUI Lang nodes into UI on screen.

See [Defining Components](/docs/openui-lang/defining-components) for the full `defineComponent` API and [The Renderer](/docs/openui-lang/renderer) for how the library is used at runtime.
Comment thread
rabisg marked this conversation as resolved.

### `chat-app/hooks/useStreamingChat.ts` - progressive streaming via XHR

React Native's `fetch()` does not expose `response.body` as a `ReadableStream`, so the hook uses `XMLHttpRequest` instead. `onprogress` fires repeatedly as chunks arrive:

```ts
xhr.onprogress = () => {
const newData = xhr.responseText.slice(lastLength);
lastLength = xhr.responseText.length;
accumulated += newData;
onChunk(accumulated, false);
};
```

Each callback receives the full accumulated OpenUI Lang string so far, which makes it easy to hand the entire in-progress document to `<Renderer />`. This is the main React Native-specific detail in the example.

### `chat-app/components/BotBubble.tsx` - rendering the in-progress response

Each bot message reads from its own entry in `streamStore` and passes the accumulated OpenUI Lang into `<Renderer />`:

```tsx
const content = useStreamContent(messageId);
const openui = content?.openui ?? "";
const isStreaming = content?.isStreaming ?? true;

<Renderer response={openui} library={library} isStreaming={isStreaming} />
```

Keeping streamed content in a separate store means each bubble can update independently instead of forcing the entire message list to re-render on every chunk.

See [The Renderer](/docs/openui-lang/renderer) for the full `<Renderer />` API.

### `backend/src/library.ts` - Node-compatible prompt source

This file mirrors the mobile library, but every `component` returns `null`. That lets the OpenUI CLI inspect the component definitions in a Node environment and generate `system-prompt.txt` without importing React Native.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have a common file that both backend and react-native imports that just has the zod types rather than duplicating it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cli isn't working with react-native components and importing in next is also broken at the moment.


Run `pnpm generate:prompt` any time you change:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given that we are already duplicating the types - do we need this?
cant we directly call library.toPrompt() on the backend in nextjs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

facing react-version mismatch issue in next. tried building it outside of pnpm-package, issue remained. we either ways need to fix the approach


- component names
- prop schemas
- descriptions
- prompt rules or examples

See [System Prompts](/docs/openui-lang/system-prompts) for how prompt generation works.

### `backend/src/app/api/chat/route.ts` - raw text streaming endpoint

The backend route reads `system-prompt.txt`, calls OpenAI with `stream: true`, and writes each `delta.content` chunk into a `ReadableStream` response:

```ts
for await (const chunk of completion) {
const text = chunk.choices[0]?.delta?.content ?? "";
if (text) {
controller.enqueue(new TextEncoder().encode(text));
}
}
```

The response uses `Content-Type: text/plain; charset=utf-8` and permissive CORS headers so the Expo app can call it during development.
4 changes: 3 additions & 1 deletion docs/content/docs/openui-lang/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"interactivity",
"---Advanced---",
"specification",
"benchmarks"
"benchmarks",
"---Examples---",
"examples/react-native"
]
}
Binary file added docs/public/videos/react-native-demo.mp4
Binary file not shown.
131 changes: 131 additions & 0 deletions examples/openui-react-native/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# OpenUI React Native Example

A full-stack example that demonstrates using `@openuidev/react-lang` in a React Native (Expo) app with a Next.js API backend. The LLM streams responses in [OpenUI Lang](https://www.openui.com/docs/openui-lang/overview) and the mobile client renders them as native components in real time.

<video src="../../docs/public/videos/react-native-demo.mp4"
noControls
playsInline
muted
preload="metadata"
className="h-[600px] rounded-lg m-auto"
autoPlay
loop
/>

## Architecture

```
┌─────────────────────────────┐ ┌──────────────────────────┐
│ React Native (Expo) app │ HTTP │ Next.js backend (API) │
│ │ ──────►│ │
│ • Chat UI │ │ • /api/chat (POST) │
│ • <Renderer /> parsing │◄────── │ • Streams OpenUI Lang │
│ streamed OpenUI Lang │ stream│ from GPT │
│ • Native chart components │ │ • CORS enabled │
└─────────────────────────────┘ └──────────────────────────┘
```

## Project Structure

```
openui-react-native/
├── package.json # Root workspace scripts
└── backend/ # Next.js API server
├── src/
│ ├── library.ts # Component library definition (Node-compatible)
│ ├── system-prompt.txt # Auto-generated from library.ts
│ └── app/api/chat/
│ └── route.ts # Streaming chat endpoint
└── env.example
```

> The React Native app lives in a sibling `react-native-openui/` directory (not checked in here). The root `package.json` wires both together as a pnpm workspace.

## Getting Started

### Prerequisites

- Node.js 18+
- pnpm
- An OpenAI API key

### 1. Install dependencies

```bash
pnpm install
```

### 2. Configure the backend

```bash
cp backend/env.example backend/.env.local
```

Add your key to `backend/.env.local`:

```
OPENAI_API_KEY=sk-...
```

### 3. Generate the system prompt

The [Prompt Generator](https://www.openui.com/docs/openui-lang/overview) compiles `library.ts` into `system-prompt.txt` — containing component signatures, syntax rules, and streaming guidelines for the LLM:

```bash
pnpm generate:prompt
```

### 4. Start the backend

```bash
pnpm dev:backend
```

The API will be available at `http://localhost:3000`.

### 5. Start the mobile app

```bash
pnpm dev:mobile
```

## What's in This Example

### `backend/src/library.ts`

Defines the custom component library using [`defineComponent`](https://www.openui.com/docs/openui-lang/overview) and `createLibrary`. This is a **Node-compatible** version (renderers set to `null`) used only by the CLI to generate the system prompt — the backend never renders components itself.

The library exposes five components:

| Component | Description |
| ----------- | ---------------------------------------------------------- |
| `Card` | Root container — every response is wrapped in one |
| `Text` | Text with optional `heading`, `body`, or `caption` variant |
| `BarChart` | Vertical bar chart for comparing discrete categories |
| `LineChart` | Line chart for trends over time |
| `PieChart` | Pie chart for part-to-whole proportions |

### `backend/src/app/api/chat/route.ts`

A Next.js Route Handler that:

1. Loads `system-prompt.txt` at startup
2. Forwards the conversation to the OpenAI streaming API
3. Returns raw `text/plain` chunks — intentionally simpler than SSE so React Native can consume the stream directly without a browser `EventSource`

### React Native app (`react-native-openui/`)

Uses the [`<Renderer />`](https://www.openui.com/docs/openui-lang/overview) component from `@openuidev/react-lang` to progressively parse and render the streamed OpenUI Lang output as native components.

## Scripts

| Script | Description |
| ---------------------- | ------------------------------------------------ |
| `pnpm dev:backend` | Start the Next.js API server |
| `pnpm dev:mobile` | Start the Expo dev server |
| `pnpm generate:prompt` | Regenerate `system-prompt.txt` from `library.ts` |

## Learn More

- [OpenUI Lang overview](https://www.openui.com/docs/openui-lang/overview) — core building blocks: Library, Prompt Generator, Parser, Renderer
- [`@openuidev/react-lang` package](../../packages/react-lang)
1 change: 1 addition & 0 deletions examples/openui-react-native/backend/env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OPENAI_API_KEY=sk-...
6 changes: 6 additions & 0 deletions examples/openui-react-native/backend/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
5 changes: 5 additions & 0 deletions examples/openui-react-native/backend/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {};

export default nextConfig;
25 changes: 25 additions & 0 deletions examples/openui-react-native/backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "backend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"generate:prompt": "npx @openuidev/cli generate ./src/library.ts --out ./src/system-prompt.txt"
},
"dependencies": {
"next": "^15.2.3",
"openai": "^4.90.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@openuidev/react-lang": "^0.1.3",
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19",
"typescript": "^5",
"zod": "^4.0.0"
}
}
Loading
Loading