DEV Community

Cover image for How to Add Generative UI to Any React App in 5 Minutes
michael magan
michael magan

Posted on

How to Add Generative UI to Any React App in 5 Minutes

What is Generative UI?

Traditional AI chatbots only respond with text. Generative UI takes this further by allowing AI to respond with interactive React components. Instead of just saying "Here's your todo list," the AI can render an actual working todo component that users can interact with.

With tambo-ai, you can:

  • Register components with the AI so it knows what UI elements are available
  • Define schemas that tell the AI what props each component expects
  • Let the AI choose which component to render based on user requests
  • Stream responses so components appear in real-time as they're generated

Prerequisites

Before you start, make sure you have:

  • Node.js 18+ installed
  • npm 10+ or yarn
  • A React or Next.js project (we'll show both)
  • Basic knowledge of React hooks and TypeScript
  • Understanding of component props and state management

Step 1: Install tambo-ai

npm create tambo-app@latest my-tambo-app
cd my-tambo-app
Enter fullscreen mode Exit fullscreen mode

Step 2: Get Your API Key

Get your free API key by running:

npx tambo init
Enter fullscreen mode Exit fullscreen mode

What this does:

  • Creates a tambo account if you don't have one
  • Generates an API key for your project
  • Sets up your environment variables
  • Links your local project to the tambo dashboard

Step 3: Create Your First Component

Let's create a simple email form component that the AI can render. This component demonstrates three key tambo-ai concepts:

Breaking Down the Component

1. Zod Schema Definition

Use Zod to define what props the AI can pass to your component:

export const EmailFormProps = z.object({
  subject: z.string().default(""),
  message: z.string().default(""),
  recipient: z.string().default(""),
});
Enter fullscreen mode Exit fullscreen mode

This tells the AI exactly what props this component expects. The AI uses this schema to generate appropriate props when a user asks for an email form.

2. State Management with useTamboComponentState

Use useTamboComponentState to persist data across AI conversations:

const [emailState, setEmailState] = useTamboComponentState("email-form", {
  subject,
  message,
  recipient,
  isSent: false,
});
Enter fullscreen mode Exit fullscreen mode

Unlike regular useState, this hook:

  • Persists state across AI conversations
  • Makes state available to the AI for context
  • Survives component re-renders and page refreshes
  • Uses a unique key ("email-form") to identify this component's state

3. Streaming Props with useTamboStreamingProps

Use useTamboStreamingProps to handle real-time prop updates as the AI generates them:

useTamboStreamingProps(emailState, setEmailState, {
  subject,
  message,
  recipient,
});
Enter fullscreen mode Exit fullscreen mode

This hook automatically updates your component state when the AI streams in new prop values. As the AI generates content, your form fields will populate in real-time.

4. Explicit State Updates

Notice how we maintain the complete state object in each setEmailState call:

import { useTamboComponentState } from "@tambo-ai/react";
const [emailState, setEmailState] = useTamboComponentState("email-form", {
  subject,
  message,
  recipient,
  isSent: false,
});
Enter fullscreen mode Exit fullscreen mode

This passes the user inputs to the AI, which can be used for human-in-the-loop workflows or simply to inform the AI of user interactions.

Full Component

// components/EmailForm.tsx
"use client";

import {
  useTamboComponentState,
  useTamboStreamingProps,
} from "@tambo-ai/react";
import { z } from "zod";

// Define the props schema with Zod
export const EmailFormProps = z.object({
  subject: z.string().default(""),
  message: z.string().default(""),
  recipient: z.string().default(""),
});

export type EmailFormProps = z.infer<typeof EmailFormProps>;

export function EmailForm({
  subject = "",
  message = "",
  recipient = "",
}: EmailFormProps) {
  // Use Tambo's state management to persist values
  const [emailState, setEmailState] = useTamboComponentState("email-form", {
    subject,
    message,
    recipient,
    isSent: false,
  });

  // Connect streaming props to your state
  useTamboStreamingProps(emailState, setEmailState, {
    subject,
    message,
    recipient,
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    // Simulate sending email
    setEmailState({
      subject: emailState?.subject || "",
      message: emailState?.message || "",
      recipient: emailState?.recipient || "",
      isSent: true,
    });
    setTimeout(() => {
      setEmailState({
        subject: emailState?.subject || "",
        message: emailState?.message || "",
        recipient: emailState?.recipient || "",
        isSent: false,
      });
    }, 3000);
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-4 p-4 border rounded-lg">
      <div>
        <label className="block text-sm font-medium mb-1">To:</label>
        <input
          type="email"
          value={emailState?.recipient || ""}
          onChange={(e) =>
            setEmailState({
              subject: emailState?.subject || "",
              message: emailState?.message || "",
              recipient: e.target.value,
              isSent: emailState?.isSent || false,
            })
          }
          className="w-full p-2 border rounded"
          placeholder="Enter recipient email"
        />
      </div>

      <div>
        <label className="block text-sm font-medium mb-1">Subject:</label>
        <input
          type="text"
          value={emailState?.subject || ""}
          onChange={(e) =>
            setEmailState({
              subject: e.target.value,
              message: emailState?.message || "",
              recipient: emailState?.recipient || "",
              isSent: emailState?.isSent || false,
            })
          }
          className="w-full p-2 border rounded"
          placeholder="Enter subject"
        />
      </div>

      <div>
        <label className="block text-sm font-medium mb-1">Message:</label>
        <textarea
          value={emailState?.message || ""}
          onChange={(e) =>
            setEmailState({
              subject: emailState?.subject || "",
              message: e.target.value,
              recipient: emailState?.recipient || "",
              isSent: emailState?.isSent || false,
            })
          }
          className="w-full p-2 border rounded h-24"
          placeholder="Enter your message"
        />
      </div>

      <button
        type="submit"
        className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
        disabled={emailState?.isSent}
      >
        {emailState?.isSent ? "Sent!" : "Send Email"}
      </button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Set Up the TamboProvider

The TamboProvider is the foundation of your tambo-ai application. It:

  • Connects to tambo's AI services using your API key
  • Manages component registry so the AI knows what components are available
  • Handles conversation context and message threading
  • Provides React hooks to all child components

Here's how to add the provider:

// app/layout.tsx (Next.js) or App.tsx (React)
import { TamboProvider } from "@tambo-ai/react";
import { EmailForm, EmailFormProps } from "./components/EmailForm";

const components = [
  {
    name: "EmailForm",
    description: "A form for composing and sending emails",
    component: EmailForm,
    propsSchema: EmailFormProps,
  },
];

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <TamboProvider
          apiKey={process.env.NEXT_PUBLIC_TAMBO_API_KEY!}
          components={components}
        >
          {children}
        </TamboProvider>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Understanding Component Registration

The components array tells the AI what components are available:

const components = [
  {
    name: "EmailForm", // Unique identifier for the AI
    description: "A form for composing and sending emails", // Helps AI decide when to use this component
    component: EmailForm, // The actual React component
    propsSchema: EmailFormProps, // Zod schema defining the component's props
  },
];
Enter fullscreen mode Exit fullscreen mode

How the AI Uses This Information

  1. User sends a message: "Help me write an email to my team"
  2. AI analyzes the message and sees it relates to email composition
  3. AI checks registered components and finds "EmailForm" with description "A form for composing and sending emails"
  4. AI decides to use EmailForm because it matches the user's intent
  5. AI generates props based on the EmailFormProps schema (subject, message, recipient)
  6. Component renders with AI-generated props

Step 5: Add the Chat Interface

Now let's create the user interface that allows users to chat with the AI. This interface uses two key tambo-ai hooks:

  • useTambo() - Provides access to the conversation thread and messages
  • useTamboThreadInput() - Handles user input and message submission

Hook Usage

const { thread } = useTambo(); // Get conversation history
const { value, setValue, submit, isPending } = useTamboThreadInput(); // Handle user input
Enter fullscreen mode Exit fullscreen mode

Thread Structure

The thread object contains:

  • messages - Array of all conversation messages
  • Each message has:
    • role - Either "user" or "assistant"
    • content - The raw text content
    • message - Formatted message text
    • renderedComponent - The React component (if AI chose to render one)

Message Rendering

{
  message.renderedComponent && (
    <div className="mt-3">{message.renderedComponent}</div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This is where your registered components (like EmailForm) will appear when the AI decides to use them.

Flow Summary

  1. User types message → setValue()
  2. User submits → submit() sends to AI
  3. AI processes message → decides whether to respond with text or component
  4. If component: AI selects registered component + generates props
  5. Component renders with props → message.renderedComponent
  6. User can interact with component → state persists via useTamboComponentState

Here's a complete chat experience component:

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

import { useTambo, useTamboThreadInput } from "@tambo-ai/react";

export default function HomePage() {
  const { thread } = useTambo();
  const { value, setValue, submit, isPending } = useTamboThreadInput();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!value.trim()) return;

    await submit({ streamResponse: true });
    setValue("");
  };

  return (
    <div className="max-w-2xl mx-auto p-4">
      <h1 className="text-2xl font-bold mb-6">AI Email Assistant</h1>

      {/* Chat Messages */}
      <div className="space-y-4 mb-6 min-h-[400px]">
        {thread?.messages.map((message, index) => (
          <div key={index} className="space-y-2">
            {message.role === "user" && (
              <div className="bg-blue-100 p-3 rounded-lg">
                <strong>You:</strong> {message.content}
              </div>
            )}
            {message.role === "assistant" && (
              <div className="bg-gray-100 p-3 rounded-lg">
                <strong>AI:</strong> {message.message}
                {message.renderedComponent && (
                  <div className="mt-3">{message.renderedComponent}</div>
                )}
              </div>
            )}
          </div>
        ))}
      </div>

      {/* Input Form */}
      <form onSubmit={handleSubmit} className="flex gap-2">
        <input
          type="text"
          value={value}
          onChange={(e) => setValue(e.target.value)}
          placeholder="Try: 'Help me write an email to my team about our project update'"
          className="flex-1 p-2 border rounded"
          disabled={isPending}
        />
        <button
          type="submit"
          disabled={isPending || !value.trim()}
          className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 disabled:opacity-50"
        >
          {isPending ? "..." : "Send"}
        </button>
      </form>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Set Up Environment Variables

Create a .env.local file in your project root:

NEXT_PUBLIC_TAMBO_API_KEY=your_api_key_here
Enter fullscreen mode Exit fullscreen mode

If you're using something other than Next.js, you can remove the NEXT_PUBLIC_ prefix.

Step 7: Test Your App

Start your development server:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Try these example messages:

  • "Help me write an email to my team about our project update"
  • "Create an email to schedule a meeting with John"
  • "Write a follow-up email for the client presentation"

Next Steps

Congratulations! You've successfully added generative UI to your React app. Here's what you can explore next:

  1. Add more components - Create charts, forms, dashboards, or any React component
  2. Implement streaming - Learn about real-time prop updates with useTamboStreamingProps
  3. Add external tools - Connect to APIs using Model Context Protocol (MCP)
  4. State management - Build interactive components that respond to user actions
  5. Advanced features - Explore suggestions, thread management, and custom instructions

Resources


⭐ Star the repo if this helped you: tambo-ai

Top comments (0)