Generative UI: Streaming React Server Components with AI for Dynamic Interfaces
The chat interface is evolving. We started with text, then markdown. Now, we are entering the era of Generative UI. Instead of just describing a flight itinerary in text, the AI renders a fully interactive flight booking card. Instead of explaining a stock trend, it renders a live TradingView chart. This guide explores how to build these experiences using Next.js 15, React Server Components (RSC), and the Vercel AI SDK.
The Concept: AI as a UI Architect
Traditionally, UI is deterministic. You build a `FlightCard` component and render it when `data.type === 'flight'`. With Generative UI, the LLM decides which component to render and with what props, based on the user's natural language query.
Streaming Components
The magic happens via RSC streaming. The AI doesn't just send a JSON blob; it streams the React Component tree itself. This means the user sees the skeleton loader, then the partial UI, then the final interactive widget—all orchestrated by the LLM.
Security Risks: XSS & Hallucinations
Allowing an AI to generate UI sounds dangerous—and it can be.
Cross-Site Scripting (XSS)
If you allow the LLM to generate raw HTML (e.g., via `dangerouslySetInnerHTML`), it will eventually generate `<script>alert(1)</script>`.Solution: Never let the LLM write code. Let it call Tools that map to pre-built, safe React Components.
Hallucinated Actions
An LLM might render a "Buy Now" button with a price of $0.00 because it hallucinated a discount.Solution: The UI rendered by the AI must be stateless or validated by the backend before any transaction occurs. The "Buy" button should trigger a Server Action that re-validates the price.
Next.js Implementation: createStreamableUI
Here is a Server Action that streams a stock chart based on a user prompt.
'use server';
import { createStreamableUI } from 'ai/rsc';
import { OpenAI } from 'openai';
import { StockChart } from '@/components/StockChart';
import { Spinner } from '@/components/Spinner';
const openai = new OpenAI();
export async function submitUserMessage(content: string) {
const ui = createStreamableUI(<Spinner />);
(async () => {
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content }],
tools: [
{
type: 'function',
function: {
name: 'show_stock_price',
description: 'Get stock price and show chart',
parameters: {
type: 'object',
properties: {
symbol: { type: 'string' },
},
required: ['symbol'],
},
},
},
],
});
const toolCall = response.choices[0].message.tool_calls?.[0];
if (toolCall && toolCall.function.name === 'show_stock_price') {
const args = JSON.parse(toolCall.function.arguments);
const data = await fetchStockData(args.symbol); // Fetch from external API
// Replace the spinner with the actual component
ui.done(<StockChart symbol={args.symbol} data={data} />);
} else {
ui.done(<div className="text-gray-500">I could not find a chart for that.</div>);
}
})();
return {
id: Date.now(),
display: ui.value,
};
}Backend Data Provider (Spring Boot)
The Next.js server needs a reliable source of truth. Your Spring Boot backend acts as the data provider that ensures the numbers shown in the Generative UI are accurate and authorized.
@RestController
@RequestMapping("/api/stocks")
public class StockController {
@GetMapping("/{symbol}")
@PreAuthorize("hasRole('USER')") // Ensure user has access
public StockData getStock(@PathVariable String symbol) {
// In a real app, this calls an exchange API
return new StockData(symbol, 150.25, List.of(140.0, 145.0, 150.25));
}
}
// The UI will fetch this data, or the Next.js server will proxy it.
// Key Point: The AI *decides* to show the chart, but the *data* comes from here.The GenUI Lifecycle
Intent Recognition
LLM analyzes user prompt: "Show me AAPL stock".
Tool Selection
LLM selects `show_stock_price` tool.
Component Hydration
Next.js renders `<StockChart />` on the server and streams HTML.
Conclusion
Generative UI represents the shift from "Command Line Interface" thinking to "Intent Oriented Interface" thinking. By combining the reasoning power of LLMs with the component model of React, we can build interfaces that adapt to the user, rather than forcing the user to adapt to the interface.