-
Notifications
You must be signed in to change notification settings - Fork 606
React Native example #334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
React Native example #334
Changes from all commits
a9d518e
9ccf328
5bf1744
4311578
c0d7e81
e3b1843
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) | ||
|
|
||
| <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. | ||
|
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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. given that we are already duplicating the types - do we need this?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
| 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) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| OPENAI_API_KEY=sk-... |
| 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. |
| 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; |
| 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" | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.