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
Step 2: Get Your API Key
Get your free API key by running:
npx tambo init
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(""),
});
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,
});
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,
});
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,
});
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>
);
}
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>
);
}
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
},
];
How the AI Uses This Information
- User sends a message: "Help me write an email to my team"
- AI analyzes the message and sees it relates to email composition
- AI checks registered components and finds "EmailForm" with description "A form for composing and sending emails"
- AI decides to use EmailForm because it matches the user's intent
-
AI generates props based on the
EmailFormProps
schema (subject, message, recipient) - 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
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>
);
}
This is where your registered components (like EmailForm) will appear when the AI decides to use them.
Flow Summary
- User types message →
setValue()
- User submits →
submit()
sends to AI - AI processes message → decides whether to respond with text or component
- If component: AI selects registered component + generates props
- Component renders with props →
message.renderedComponent
- 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>
);
}
Step 6: Set Up Environment Variables
Create a .env.local
file in your project root:
NEXT_PUBLIC_TAMBO_API_KEY=your_api_key_here
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
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:
- Add more components - Create charts, forms, dashboards, or any React component
-
Implement streaming - Learn about real-time prop updates with
useTamboStreamingProps
- Add external tools - Connect to APIs using Model Context Protocol (MCP)
- State management - Build interactive components that respond to user actions
- Advanced features - Explore suggestions, thread management, and custom instructions
Resources
⭐ Star the repo if this helped you: tambo-ai
Top comments (0)