createStreamableUI is a Vercel AI SDK function that streams React Server Components from the server to the client as they render, letting you build generative UI where the interface itself is the LLM’s output. Instead of streaming text tokens and parsing them on the client, you stream actual React component trees that hydrate incrementally. This shifts the rendering responsibility to the server while preserving interactivity on the client.
How createStreamableUI works
The function wraps a React Server Component and returns a streamable value that the AI SDK’s streamUI or streamObject can send over the wire. Under the hood, it uses React’s server component streaming protocol — the same mechanism that powers Suspense boundaries in Next.js App Router — but exposes it as a first-class tool for LLM-driven UI generation.
// server action or route handler
import { createStreamableUI } from 'ai/rsc';
import { streamUI } from 'ai';
export async function generateDashboard(prompt: string) {
const { value } = await streamUI({
model: openai('gpt-4o'),
system: 'You generate dashboard widgets as React components.',
prompt,
tools: {
renderWidget: createStreamableUI({
// The component to stream — must be a Server Component
component: async function Widget({ title, data }: { title: string; data: unknown }) {
// This runs on the server, can fetch data, access DB, etc.
return (
<section className="widget">
<h3>{title}</h3>
<pre>{JSON.stringify(data, null, 2)}</pre>
</section>
);
},
}),
},
});
return value; // StreamableUIValue sent to client
}
The client receives a StreamableUIValue that resolves to a React element as chunks arrive. You render it with the StreamableUI client component:
// client component
'use client';
import { StreamableUI } from 'ai/rsc';
export function DashboardView({ stream }: { stream: StreamableUIValue }) {
return <StreamableUI value={stream} />;
}
Each tool call that returns a createStreamableUI result streams its component tree independently. The client renders partial UI as soon as the first bytes arrive — no waiting for the full response.
Why this matters for generative UI
Traditional chat interfaces stream text, then parse markdown or JSON on the client to render components. That approach has three problems:
- Round-trip latency: The model outputs text, the client parses it, then renders. Any parsing error breaks the UI.
- No server-side data access: The model can’t fetch fresh data during generation unless you build a separate tool-calling loop.
- Fragile contracts: Schema changes in your components require coordinated updates to both the model’s output format and the client parser.
createStreamableUI eliminates the parsing layer. The model calls a tool that is the component. The server executes the component — including any async data fetching — and streams the resulting React tree directly. The client just renders what arrives.
This enables patterns like:
- Progressive dashboards: The model emits a skeleton, then streams in charts as their data resolves.
- Dynamic forms: Each field is a streamed component that can validate against server state.
- Multi-step workflows: The model streams a step, waits for user input, then streams the next — all within one server action.
Concrete example: streaming a data-driven report
Here’s a complete pattern for a report generator where each section fetches its own data.
// app/actions/generateReport.ts
'use server';
import { createStreamableUI } from 'ai/rsc';
import { streamUI } from 'ai';
import { openai } from '@ai-sdk/openai';
import { db } from '@/lib/db';
export async function generateReport(topic: string) {
const { value } = await streamUI({
model: openai('gpt-4o'),
system: `Generate a market research report. Call renderSection for each section.`,
prompt: topic,
tools: {
renderSection: createStreamableUI({
component: async function ReportSection({
heading,
query,
}: {
heading: string;
query: string;
}) {
// Server-side data fetch — runs during streaming
const data = await db.metrics.findMany({
where: { topic: query },
take: 50,
});
const chartData = data.map(d => ({ x: d.date, y: d.value }));
return (
<section className="report-section">
<h2>{heading}</h2>
<Chart data={chartData} />
<InsightsList data={data} />
</section>
);
},
}),
},
});
return value;
}
// app/components/ReportViewer.tsx
'use client';
import { StreamableUI } from 'ai/rsc';
import { useActionState } from 'react';
import { generateReport } from '@/app/actions/generateReport';
export function ReportViewer() {
const [stream, startGeneration, pending] = useActionState(generateReport, null);
return (
<div>
<form action={startGeneration}>
<input name="topic" placeholder="Enter topic" required />
<button type="submit" disabled={pending}>
{pending ? 'Generating...' : 'Generate Report'}
</button>
</form>
{stream && <StreamableUI value={stream} />}
</div>
);
}
The model might call renderSection three times. The first section appears on screen while the second is still fetching data. The user sees meaningful UI within hundreds of milliseconds, not seconds.
Common misconceptions
“It’s just streaming HTML”
No. createStreamableUI streams React Server Component payloads — the same binary format Next.js uses for RSC streaming. The client receives a React element tree, not a string. This means:
- Components retain their type identity (props, children, refs)
- Client components inside the streamed tree hydrate correctly
- Suspense boundaries work as expected
- No
dangerouslySetInnerHTMLor custom parsers needed
“The model controls the UI directly”
The model controls which components render and what props they receive. It does not write arbitrary JSX. You define the component; the model chooses when to invoke it and with what arguments. This is a critical security boundary — the model cannot inject <script> tags or arbitrary event handlers.
“It replaces client-side state”
Streamed UI is server-initiated. For user interactions (clicks, inputs, local state), you still need client components. The typical pattern: stream a server component that renders client components for interactive parts.
// Server component streamed via createStreamableUI
async function SearchResults({ query }: { query: string }) {
const results = await search(query);
return (
<div>
{results.map(r => (
// Client component for interactivity
<ResultCard key={r.id} initialData={r} />
))}
</div>
);
}
// Client component — hydrates independently
'use client';
export function ResultCard({ initialData }) {
const [expanded, setExpanded] = useState(false);
// ... interactive logic
}
“It works with any model”
The model must support tool calling with structured outputs. OpenAI, Anthropic, and Gemini work. Smaller local models often don’t reliably emit the tool calls needed. If your model can’t call renderSection with valid JSON arguments, the stream breaks.
“It’s only for chat”
streamUI works anywhere you can run a server action — form submissions, scheduled jobs, webhook handlers, WebSocket messages. The “chat” metaphor is just one invocation pattern.
Performance considerations
Streaming React components has real costs:
- Server CPU: Each streamed component executes on the server. Complex components with heavy computations block the stream. Keep components lightweight; push heavy work to dedicated API routes or edge functions.
- Payload size: RSC payloads are larger than JSON. A component tree with many nodes streams more bytes than a compact JSON schema. Compression (gzip/brotli) helps significantly — ensure your edge/CDN compresses responses.
- Hydration cost: Client components inside streamed trees hydrate on the client. If you stream 50 interactive cards, you pay 50 hydration costs. Consider streaming static server components and lazy-loading interactive wrappers.
// Better: stream static content, hydrate interaction on demand
async function Feed({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>
<StaticSummary data={item} />
<LazyInteractiveActions itemId={item.id} />
</li>
))}
</ul>
);
}
Error handling
If a streamed component throws, the error propagates through the RSC stream. Wrap components in error boundaries on the server:
import { createStreamableUI } from 'ai/rsc';
tools: {
renderWidget: createStreamableUI({
component: async function Widget({ id }) {
try {
const data = await fetchWidgetData(id);
return <WidgetView data={data} />;
} catch (e) {
// This becomes part of the stream — client renders it
return <WidgetError widgetId={id} message={e.message} />;
}
},
}),
}
The client StreamableUI component also accepts a fallback prop for stream-level errors (network failure, model timeout):
<StreamableUI value={stream} fallback={<div>Generation failed. Retry?</div>} />
When not to use it
- Static UIs: If the component tree is known at build time, don’t stream it. Use static RSC or SSG.
- High-frequency updates: For real-time dashboards updating multiple times per second, WebSockets + client state beat RSC streaming.
- Non-React clients: Mobile apps, CLIs, or non-React frontends can’t consume
StreamableUIpayloads. Expose a JSON API alongside.
Summary
createStreamableUI bridges the gap between LLM tool calling and React Server Components. It lets the model emit UI components directly, with server-side data access, streaming hydration, and no client-side parsing layer. The trade-off is server compute and payload size — use it where progressive, data-driven UI generation adds real value, not as a default for all model outputs.