Back to roadmaps vercel-ai-sdk Course

Generative UI: Streaming React Components

Traditional AI chatbots only return plain text or markdown lists. Generative UI allows LLMs to trigger interactive React components (such as graphs, maps, or checkout panels) in response to user queries.


1. Concept Architecture

Instead of returning text summaries, the backend tool execution outputs structured parameters. The frontend reads these properties and renders a matched React component dynamically.

graph TD
    A[User: What is my stock portfolio status?] --> B[AI decides to invoke showPortfolioChart tool]
    B --> C[Backend returns raw JSON dataset]
    C --> D[React UI reads data stream]
    D --> E[Renders custom dynamic Recharts UI Component]

2. Implementing Tool Component Rendering on the Client

On the client side, read the toolInvocations property inside your message object loop:

// app/chat/GenUIConsole.tsx
"use client";

import { useChat } from "ai/react";
import { StockChart } from "../components/StockChart";

export default function GenUIConsole() {
  const { messages, input, handleInputChange, handleSubmit } = useChat({
    api: "/api/chat-genui",
  });

  return (
    <div className="max-w-xl mx-auto p-4 border rounded-2xl">
      <div className="space-y-4 h-[400px] overflow-y-auto">
        {messages.map((message) => (
          <div key={message.id} className="p-3 bg-gray-50 rounded-lg">
            <strong>{message.role === "user" ? "User" : "Agent"}:</strong>
            
            {/* 1. Render text content if present */}
            {message.content && <p className="mt-1">{message.content}</p>}

            {/* 2. Process tool calls and render custom components */}
            {message.toolInvocations?.map((toolInvocation) => {
              const { toolName, toolCallId, state } = toolInvocation;

              if (state === "result") {
                const { result } = toolInvocation;

                if (toolName === "displayStockPerformance") {
                  // Render a React Component using retrieved results data
                  return (
                    <div key={toolCallId} className="mt-3">
                      <StockChart data={result.stockPoints} title={result.companyName} />
                    </div>
                  );
                }
              } else {
                return <div key={toolCallId} className="text-gray-400">Loading chart data...</div>;
              }
              return null;
            })}
          </div>
        ))}
      </div>

      <form onSubmit={handleSubmit} className="flex gap-2 mt-4">
        <input value={input} onChange={handleInputChange} className="flex-1 border p-2 rounded-lg" />
        <button type="submit" className="bg-indigo-600 text-white px-4 py-2 rounded-lg">Send</button>
      </form>
    </div>
  );
}

3. Implementing the API Route

Ensure your backend route defines the parameters required by the frontend React components:

// app/api/chat-genui/route.ts
import { streamText, tool } from "ai";
import { z } from "zod";
import { defaultModel } from "../../../lib/models";

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = await streamText({
    model: defaultModel,
    messages,
    tools: {
      displayStockPerformance: tool({
        description: "Display historical stock performance for a company ticker.",
        parameters: z.object({
          ticker: z.string(),
        }),
        execute: async ({ ticker }) => {
          // Mock data returned to frontend
          return {
            companyName: ticker.toUpperCase(),
            stockPoints: [120, 125, 122, 131, 145],
          };
        },
      }),
    },
  });

  return result.toDataStreamResponse();
}
Published on Last updated: