n4nAI

Generative UI with Vercel AI SDK and React Server Components

Practical vercel ai sdk generative ui rsc tutorial: build streaming React Server Components that render UI from LLM tool calls with runnable code.

n4n Team3 min read562 words

Audio narration

Coming soon — every post will get a voice note here.

This vercel ai sdk generative ui rsc tutorial shows how to let a language model return interactive React Server Components instead of just text. We’ll build a chat surface where a tool call streams a weather card rendered on the server, using Next.js App Router and the Vercel AI SDK’s streamUI.

Prerequisites

  • Node.js 18.18 or later
  • Next.js 14.2+ with App Router and TypeScript
  • An API key from OpenAI or any OpenAI-compatible gateway
  • Familiarity with React Server Components and server actions

Install the required packages:

npm install ai @ai-sdk/openai @ai-sdk/react zod

Set your provider key in .env.local:

OPENAI_API_KEY=sk-...

Project Structure

We’ll keep five files:

  • app/weather.tsx – presentational server component
  • app/actions.tsx – server action using streamUI
  • app/ai.tsxcreateAI wrapper for client hooks
  • app/layout.tsx – root layout wrapping the AI provider
  • app/page.tsx – client chat UI

Build the Server Component

The component receives a city and optional data. It renders a loading placeholder, then the resolved card.

// app/weather.tsx
export function Weather({ city, data }: { city: string; data?: { temp: number; condition: string } }) {
  if (!data) {
    return <div className="card">Loading weather for {city}…</div>;
  }
  return (
    <div className="card">
      <h3>{city}</h3>
      <p>{data.temp}°C, {data.condition}</p>
    </div>
  );
}

Define the Generative Action

streamUI accepts a model, prompt, and a tools map. Each tool’s generate function yields intermediate UI and returns the final component.

// app/actions.tsx
'use server';

import { streamUI } from 'ai/rsc';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
import { Weather } from './weather';

async function getWeather(city: string) {
  // stubbed; replace with real fetch
  return { temp: 21, condition: 'Partly cloudy' };
}

export async function submitMessage(input: string) {
  const result = await streamUI({
    model: openai('gpt-4o'),
    prompt: input,
    text: ({ content }) => <p>{content}</p>,
    tools: {
      showWeather: {
        parameters: z.object({ city: z.string() }),
        generate: async function* ({ city }) {
          yield <Weather city={city} />;
          const data = await getWeather(city);
          return <Weather city={city} data={data} />;
        },
      },
    },
  });

  return result.value;
}

The text option renders plain model output as a paragraph. The generate generator yields a loading state, then returns the filled card.

Wire Client Hooks

The Vercel AI SDK exposes useUIState and useActions after wrapping your app in createAI.

// app/ai.tsx
'use client';

import { createAI } from '@ai-sdk/react';
import { submitMessage } from './actions';

export const AI = createAI({
  actions: { submitMessage },
  initialUIState: [],
  initialAIState: [],
});

Wrap the root layout:

// app/layout.tsx
import { AI } from './ai';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <AI>{children}</AI>
      </body>
    </html>
  );
}

Now the chat page:

// app/page.tsx
'use client';

import { useState } from 'react';
import { useUIState, useActions } from '@ai-sdk/react';

export default function Page() {
  const [input, setInput] = useState('');
  const [messages, setMessages] = useUIState();
  const { submitMessage } = useActions();

  async function send(e: React.FormEvent) {
    e.preventDefault();
    const value = input.trim();
    if (!value) return;
    setInput('');

    setMessages((prev) => [...prev, <div key={prev.length}>You: {value}</div>]);
    const ui = await submitMessage(value);
    setMessages((prev) => [...prev, ui]);
  }

  return (
    <main>
      <div id="messages">{messages}</div>
      <form onSubmit={send}>
        <input value={input} onChange={(e) => setInput(e.target.value)} placeholder="Ask for weather…" />
        <button type="submit">Send</button>
      </form>
    </main>
  );
}

Understand UI and AI State

The createAI call separates transient UI from serializable conversation state. In a real app you’d type these and persist AI state for replay.

// app/ai.tsx (extended)
'use client';
import { createAI } from '@ai-sdk/react';
import { submitMessage } from './actions';

export type AIState = { role: 'user' | 'assistant'; content: string }[];
export type UIState = React.ReactNode[];

export const AI = createAI<AIState, UIState>({
  actions: { submitMessage },
  initialAIState: [],
  initialUIState: [],
});

You would then use onFinish inside streamUI to push finalized text into AI state. We omit that here to keep the vercel ai sdk generative ui rsc tutorial focused on the rendering path.

Add a Second Tool

Extending to multiple generative surfaces is straightforward. Add a stock quote tool:

// inside tools map in app/actions.tsx
showStock: {
  parameters: z.object({ symbol: z.string() }),
  generate: async function* ({ symbol }) {
    yield <div>Loading {symbol}…</div>;
    const price = await fakeStock(symbol);
    return <div>{symbol}: ${price}</div>;
  },
},

The model decides which tool to call based on the prompt. The client receives whatever React node the tool returns, with no extra client bundling for the data fetch.

Run and Verify

Start the dev server:

npm run dev

Open http://localhost:3000. Type What's the weather in Oslo?. Expected output in the messages container:

You: What's the weather in Oslo?
Loading weather for Oslo…
Oslo
21°C, Partly cloudy

The loading card appears immediately, then swaps to the resolved data when the tool call completes. Inspect the Network tab: you’ll see RSC payload chunks, not a JSON weather API response. No client-side fetch of the weather source occurs; the component renders on the server.

Swap to an OpenAI-Compatible Gateway

If you want one endpoint that fronts 240+ models with automatic fallback when a provider is rate-limited, point the SDK at n4n.ai’s OpenAI-compatible route. Change the model import:

import { createOpenAI } from '@ai-sdk/openai';

const openai = createOpenAI({
  baseURL: 'https://api.n4n.ai/v1',
  apiKey: process.env.N4N_API_KEY,
});

// then use openai('gpt-4o') as before

The rest of the vercel ai sdk generative ui rsc tutorial code stays identical. The gateway forwards cache-control hints and honors routing directives, so server-side streaming behaves the same.

Production Caveats

RSC streaming over server actions works, but watch payload size. Each yielded component serializes to the RSC wire format; large trees inflate latency. Keep generative UI small and idempotent.

Tool calls that throw should be caught inside generate and yield an error component. streamUI does not automatically surface exceptions to the client as UI.

Model providers vary in tool-call reliability. Test the exact model you pin. In this vercel ai sdk generative ui rsc tutorial we used gpt-4o, but the pattern holds for any model that supports structured tool calls.

That’s the full loop: client sends prompt, server action streams UI, React merges it into the tree. You now have a foundation to extend with multiple tools and richer components.

Tagsvercel-ai-sdkgenerative-uireact-server-componentsrsc

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All vercel ai sdk generative ui with react server components posts →