Implementing vercel ai sdk rsc streaming react server components changes how you ship LLM features: instead of sending a JSON blob to the client and rendering after the fact, you stream React nodes from the server as tokens arrive. This guide builds a working Next.js App Router app that calls an LLM and renders streamed components, end to end.
Step 1: Scaffold a Next.js App Router project
Start with a clean Next.js 14+ app using the App Router. RSC is enabled by default; you do not need to opt in.
npx create-next-app@latest rsc-stream-demo --app --ts --tailwind --no-src-dir
cd rsc-stream-demo
The generated app/ directory uses server components by default. Server actions are defined in files with 'use server'. No extra webpack or babel config is required for RSC streaming. If you later deploy to a platform that buffers responses, set export const dynamic = 'force-dynamic' at the page level to avoid static optimization.
Step 2: Install the Vercel AI SDK and provider packages
The RSC streaming primitives live in the ai package under ai/rsc. You also need a model provider and a schema validator for tool calls.
npm install ai@^3 @ai-sdk/openai@^1 zod
@ai-sdk/openai exposes the OpenAI-compatible interface. zod is used to type tool parameters inside streamUI. Pin major versions that match your SDK major; the RSC API stabilized in AI SDK 3.0 and remains in 3.x.
Step 3: Configure the model endpoint
By default the OpenAI provider points at api.openai.com. To use an OpenAI-compatible gateway, instantiate it with a custom baseURL.
// lib/provider.ts
import { createOpenAI } from '@ai-sdk/openai';
export const openai = createOpenAI({
baseURL: 'https://api.n4n.ai/v1', // OpenAI-compatible endpoint
apiKey: process.env.N4N_API_KEY,
});
If you route through n4n.ai, its OpenAI-compatible endpoint works with the same provider setup and adds automatic fallback when a provider is rate-limited or degraded. Store the key locally:
echo "N4N_API_KEY=sk-your-key" > .env.local
You can also pass headers to forward provider cache-control hints. The gateway honors client routing directives, so you can pin a model or let it fall back across 240+ models without changing application code.
Step 4: Write a server action that streams UI
Create app/actions.tsx. Mark the file 'use server' and use streamUI to return a streamable React node. The text renderer receives incremental content; tools let the model emit structured component streams.
// app/actions.tsx
'use server';
import { streamUI } from 'ai/rsc';
import { openai } from '@/lib/provider';
import { z } from 'zod';
export async function generateUI(prompt: string) {
const result = await streamUI({
model: openai('gpt-4o-mini'),
system: 'You are a concise assistant that renders UI.',
prompt,
text: ({ content }) => (
<div className="whitespace-pre-wrap rounded bg-zinc-900 p-3 text-sm">
{content}
</div>
),
tools: {
weather: {
parameters: z.object({ city: z.string() }),
generate: async function* ({ city }) {
yield <p className="text-xs">Loading {city}…</p>;
const res = await fetch(`https://api.weather.example/${city}`);
const data = await res.json();
return <p>{city}: {data.temp}°C</p>;
},
},
},
});
return result.value;
}
streamUI returns { value } where value is a streamable RSC payload. The server keeps the connection open and pushes React nodes as the model generates text or invokes tools. When building vercel ai sdk rsc streaming react server components, this server action is the only network boundary the client touches.
Step 5: Build the client component
The client component calls the action and renders the returned node directly. Wrap the call in a transition to track pending state.
// app/chat.tsx
'use client';
import { useState, useTransition } from 'react';
import { generateUI } from './actions';
export default function Chat() {
const [input, setInput] = useState('');
const [ui, setUi] = useState<React.ReactNode>(null);
const [isPending, startTransition] = useTransition();
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
startTransition(async () => {
const node = await generateUI(input);
setUi(node);
});
}
return (
<div className="space-y-4">
<form onSubmit={handleSubmit} className="flex gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
className="flex-1 rounded border p-2"
placeholder="Ask something…"
/>
<button type="submit" disabled={isPending} className="rounded bg-blue-600 px-4">
{isPending ? 'Streaming…' : 'Generate'}
</button>
</form>
<div>{ui}</div>
</div>
);
}
No useEffect, no JSON parsing, no client-side model client. The streamed React node is hydrated and inserted into the tree as chunks arrive.
Step 6: Wire the page and force dynamic rendering
In app/page.tsx, import the client component and disable static caching so the stream isn’t buffered at build time.
// app/page.tsx
import Chat from './chat';
export const dynamic = 'force-dynamic';
export default function Page() {
return (
<main className="mx-auto max-w-2xl p-6">
<h1 className="mb-4 text-xl font-bold">
Vercel AI SDK RSC streaming demo
</h1>
<Chat />
</main>
);
}
If you deploy to an edge runtime, add export const runtime = 'edge' to the action file. Edge runtimes handle streaming well but restrict some Node APIs; keep tool fetches compatible.
Step 7: Verify the stream
Run npm run dev, open the local URL, and submit a prompt.
Verification checklist:
- DevTools → Network shows a
fetchto the server action with a200status and chunked transfer encoding. - The text node appears incrementally in the browser, not as a single paint after a delay.
- Triggering the
weathertool shows the loading paragraph first, then the resolved data. - The client bundle contains no model credentials or provider URLs; all inference happens server-side.
If you see the full response only after a pause, confirm you are returning result.value from streamUI (not generateText) and that no reverse proxy between you and the browser buffers the response.
How vercel ai sdk rsc streaming react server components differs from useChat
The useChat hook ships message arrays to the client and lets you render with client hooks. With vercel ai sdk rsc streaming react server components, the server ships rendered React nodes. That means you can embed server-only data fetches, auth checks, and tool UIs without exposing them to the client bundle. The tradeoff: the client loses fine-grained control over message state unless you wrap the streamed node in client state.
Use RSC streaming when the UI shape depends on model output and you want to keep logic on the server. Use useChat when you need client-side message editing, retries, or optimistic UI.
Error handling and boundaries
Streamed RSC nodes can throw during generation. Wrap the rendered node in a React error boundary on the client to catch failures without crashing the page.
'use client';
import { ErrorBoundary } from 'react-error-boundary';
<ErrorBoundary fallback={<p>Stream failed.</p>}>
{ui}
</ErrorBoundary>
On the server, streamUI accepts onError to log or transform errors before they reach the client. Use it to strip internal details.
Production notes
- Streaming RSC improves perceived latency but increases TTFB. Monitor payload sizes; large tool outputs bloat the RSC stream.
- The
streamUIcall supportsonFinishfor logging token usage. If you meter per token, forward the usage to your billing system there. - When using an OpenAI-compatible gateway, honor provider cache-control hints by passing
headersincreateOpenAI. Gateways that forward cache-control hints let repeated prompts hit cached completions, cutting cost and latency.
You now have a running vercel ai sdk rsc streaming react server components pipeline that keeps model logic server-side while delivering live UI to the browser.