This vercel ai sdk generative ui rsc tutorial shows how to let a language model return interactive React Server Components instead of just text. We’ll build a chat surface where a tool call streams a weather card rendered on the server, using Next.js App Router and the Vercel AI SDK’s streamUI.
Prerequisites
- Node.js 18.18 or later
- Next.js 14.2+ with App Router and TypeScript
- An API key from OpenAI or any OpenAI-compatible gateway
- Familiarity with React Server Components and server actions
Install the required packages:
npm install ai @ai-sdk/openai @ai-sdk/react zod
Set your provider key in .env.local:
OPENAI_API_KEY=sk-...
Project Structure
We’ll keep five files:
app/weather.tsx– presentational server componentapp/actions.tsx– server action usingstreamUIapp/ai.tsx–createAIwrapper for client hooksapp/layout.tsx– root layout wrapping the AI providerapp/page.tsx– client chat UI
Build the Server Component
The component receives a city and optional data. It renders a loading placeholder, then the resolved card.
// app/weather.tsx
export function Weather({ city, data }: { city: string; data?: { temp: number; condition: string } }) {
if (!data) {
return <div className="card">Loading weather for {city}…</div>;
}
return (
<div className="card">
<h3>{city}</h3>
<p>{data.temp}°C, {data.condition}</p>
</div>
);
}
Define the Generative Action
streamUI accepts a model, prompt, and a tools map. Each tool’s generate function yields intermediate UI and returns the final component.
// app/actions.tsx
'use server';
import { streamUI } from 'ai/rsc';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
import { Weather } from './weather';
async function getWeather(city: string) {
// stubbed; replace with real fetch
return { temp: 21, condition: 'Partly cloudy' };
}
export async function submitMessage(input: string) {
const result = await streamUI({
model: openai('gpt-4o'),
prompt: input,
text: ({ content }) => <p>{content}</p>,
tools: {
showWeather: {
parameters: z.object({ city: z.string() }),
generate: async function* ({ city }) {
yield <Weather city={city} />;
const data = await getWeather(city);
return <Weather city={city} data={data} />;
},
},
},
});
return result.value;
}
The text option renders plain model output as a paragraph. The generate generator yields a loading state, then returns the filled card.
Wire Client Hooks
The Vercel AI SDK exposes useUIState and useActions after wrapping your app in createAI.
// app/ai.tsx
'use client';
import { createAI } from '@ai-sdk/react';
import { submitMessage } from './actions';
export const AI = createAI({
actions: { submitMessage },
initialUIState: [],
initialAIState: [],
});
Wrap the root layout:
// app/layout.tsx
import { AI } from './ai';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<AI>{children}</AI>
</body>
</html>
);
}
Now the chat page:
// app/page.tsx
'use client';
import { useState } from 'react';
import { useUIState, useActions } from '@ai-sdk/react';
export default function Page() {
const [input, setInput] = useState('');
const [messages, setMessages] = useUIState();
const { submitMessage } = useActions();
async function send(e: React.FormEvent) {
e.preventDefault();
const value = input.trim();
if (!value) return;
setInput('');
setMessages((prev) => [...prev, <div key={prev.length}>You: {value}</div>]);
const ui = await submitMessage(value);
setMessages((prev) => [...prev, ui]);
}
return (
<main>
<div id="messages">{messages}</div>
<form onSubmit={send}>
<input value={input} onChange={(e) => setInput(e.target.value)} placeholder="Ask for weather…" />
<button type="submit">Send</button>
</form>
</main>
);
}
Understand UI and AI State
The createAI call separates transient UI from serializable conversation state. In a real app you’d type these and persist AI state for replay.
// app/ai.tsx (extended)
'use client';
import { createAI } from '@ai-sdk/react';
import { submitMessage } from './actions';
export type AIState = { role: 'user' | 'assistant'; content: string }[];
export type UIState = React.ReactNode[];
export const AI = createAI<AIState, UIState>({
actions: { submitMessage },
initialAIState: [],
initialUIState: [],
});
You would then use onFinish inside streamUI to push finalized text into AI state. We omit that here to keep the vercel ai sdk generative ui rsc tutorial focused on the rendering path.
Add a Second Tool
Extending to multiple generative surfaces is straightforward. Add a stock quote tool:
// inside tools map in app/actions.tsx
showStock: {
parameters: z.object({ symbol: z.string() }),
generate: async function* ({ symbol }) {
yield <div>Loading {symbol}…</div>;
const price = await fakeStock(symbol);
return <div>{symbol}: ${price}</div>;
},
},
The model decides which tool to call based on the prompt. The client receives whatever React node the tool returns, with no extra client bundling for the data fetch.
Run and Verify
Start the dev server:
npm run dev
Open http://localhost:3000. Type What's the weather in Oslo?. Expected output in the messages container:
You: What's the weather in Oslo?
Loading weather for Oslo…
Oslo
21°C, Partly cloudy
The loading card appears immediately, then swaps to the resolved data when the tool call completes. Inspect the Network tab: you’ll see RSC payload chunks, not a JSON weather API response. No client-side fetch of the weather source occurs; the component renders on the server.
Swap to an OpenAI-Compatible Gateway
If you want one endpoint that fronts 240+ models with automatic fallback when a provider is rate-limited, point the SDK at n4n.ai’s OpenAI-compatible route. Change the model import:
import { createOpenAI } from '@ai-sdk/openai';
const openai = createOpenAI({
baseURL: 'https://api.n4n.ai/v1',
apiKey: process.env.N4N_API_KEY,
});
// then use openai('gpt-4o') as before
The rest of the vercel ai sdk generative ui rsc tutorial code stays identical. The gateway forwards cache-control hints and honors routing directives, so server-side streaming behaves the same.
Production Caveats
RSC streaming over server actions works, but watch payload size. Each yielded component serializes to the RSC wire format; large trees inflate latency. Keep generative UI small and idempotent.
Tool calls that throw should be caught inside generate and yield an error component. streamUI does not automatically surface exceptions to the client as UI.
Model providers vary in tool-call reliability. Test the exact model you pin. In this vercel ai sdk generative ui rsc tutorial we used gpt-4o, but the pattern holds for any model that supports structured tool calls.
That’s the full loop: client sends prompt, server action streams UI, React merges it into the tree. You now have a foundation to extend with multiple tools and richer components.