Generative UI: Streaming React Server Components with AI for Dynamic Interfaces

📅 December 20, 2025⏱️ 30 min read🏷️ Frontend AI

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.

actions.tsx (Server Action)
'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.

Java (Spring Boot)
@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

1

Intent Recognition

LLM analyzes user prompt: "Show me AAPL stock".

2

Tool Selection

LLM selects `show_stock_price` tool.

3

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.

🌌
Purple Dream
Active Theme