How to Create AI-Powered Applications with Dynamic UI in React Using Tambo
Tambo's React SDK converts LLM-generated instructions into live UI components by maintaining a registry of React components that the AI references, streaming props as they arrive, and rendering them through a hierarchy of context providers.
To create AI-powered applications with dynamic UI in React using Tambo, you wrap your application in the TamboProvider from the @tambo-ai/react package. This provider initializes a component registry that maps LLM output to registered React components, enabling the AI to render interactive elements directly within the chat interface.
Architecture Overview
Tambo implements dynamic UI generation through a layered provider architecture defined in react-sdk/src/v1/providers/tambo-v1-provider.tsx. Each provider handles a specific concern:
TamboProvider– The main entry point that composes all sub-providers and holds static SDK configuration.TamboClientProvider– Creates the low-level@tambo-ai/typescript-sdkclient and handles authentication.TamboRegistryProvider– Stores component and tool definitions, MCP server metadata, and static resources (source:react-sdk/src/providers/tambo-registry-provider.tsx).TamboStreamProvider– Coordinates per-thread streaming state, run-ids, and message handling (source:react-sdk/src/v1/providers/tambo-v1-stream-context.tsx).TamboThreadInputProvider– Exposes the UI-sideuseTamboThreadInputhook for sending user messages.TamboMcpProvider/TamboMcpTokenProvider– Optional Model-Context-Protocol (MCP) integration for external tool access.
Core Implementation Steps
Install the SDK
Add the Tambo React SDK and optional peer dependencies for schema validation:
npm install @tambo-ai/react
npm install zod@^4 zod-to-json-schema@^3.25.1
Define UI Components with Zod Schemas
Each component must export a React component and a Zod schema describing its props. The schema enables the SDK to validate streamed props before rendering, as implemented in react-sdk/src/util/registry.ts.
// src/components/WeatherChart.tsx
import { z } from "zod";
export const WeatherChart = ({ data }: { data: { day: string; high: number }[] }) => (
<div>
{data.map((d) => (
<div key={d.day}>
{d.day}: {d.high}°C
</div>
))}
</div>
);
// Export the Zod schema for the SDK
export const WeatherChartSchema = z.object({
data: z.array(z.object({ day: z.string(), high: z.number() })),
});
Register Components and Tools
Pass the component definitions to <TamboProvider> or call registerComponent at runtime via the useTamboRegistry hook. The registry performs validation through validateAndPrepareComponent before storing the definition.
// src/App.tsx
import { TamboProvider } from "@tambo-ai/react";
import { WeatherChart, WeatherChartSchema } from "./components/WeatherChart";
const components = [
{
name: "WeatherChart",
description: "Renders a chart of weather data",
component: WeatherChart,
propsSchema: WeatherChartSchema,
},
];
export default function App() {
return (
<TamboProvider
apiKey={process.env.NEXT_PUBLIC_TAMBO_API_KEY!}
userKey="user_123"
components={components}
>
<ChatInterface />
</TamboProvider>
);
}
For client-side tools, use registerTool with an exec function:
import { useTamboRegistry } from "@tambo-ai/react";
function RegisterTools() {
const { registerTool } = useTamboRegistry();
React.useEffect(() => {
registerTool({
name: "searchWeb",
description: "Search the web for a query",
inputSchema: z.object({ query: z.string() }),
exec: async ({ query }) => {
const res = await fetch(`https://api.example.com/search?q=${encodeURIComponent(query)}`);
return await res.json();
},
});
}, [registerTool]);
return null;
}
Build the Chat Interface
Use the useTambo hook to access thread state, messages, and streaming status; use useTamboThreadInput to capture user input. These hooks pull from the context providers defined in react-sdk/src/v1/hooks/use-tambo-v1.ts and react-sdk/src/v1/hooks/use-tambo-v1-thread-input.ts.
// src/ChatInterface.tsx
import {
useTambo,
useTamboThreadInput,
Message,
LoadingIndicator,
} from "@tambo-ai/react";
export function ChatInterface() {
const { messages, isStreaming } = useTambo();
const { value, setValue, submit, isPending } = useTamboThreadInput();
return (
<form
onSubmit={async (e) => {
e.preventDefault();
await submit();
}}
>
<div className="messages">
{messages.map((msg) => (
<Message key={msg.id} message={msg} />
))}
{isStreaming && <LoadingIndicator />}
</div>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Ask about the weather…"
/>
<button disabled={isPending}>Send</button>
</form>
);
}
Handle Dynamic Rendering
When the LLM replies with a component content block, the SDK processes it through useTambo() which maintains a componentCacheRef to preserve component identity across re-renders while props stream in. The ComponentRenderer (source: react-sdk/src/v1/components/v1-component-renderer.tsx) validates props against the Zod schema using the ~standard validation protocol and wraps the component with ComponentContentProvider to enable useTamboComponentState for component-specific state management.
When the model emits:
{
"type": "component",
"name": "WeatherChart",
"props": { "data": [{ "day": "Mon", "high": 23 }] }
}
The Message component automatically renders the registered WeatherChart component with validated props, without requiring manual conditional rendering code.
Optional Features
Dynamic Resources and MCP Integration
For AI access to external knowledge bases or tools via the Model Context Protocol (MCP), provide resource handlers to TamboProvider:
<TamboProvider
apiKey={process.env.NEXT_PUBLIC_TAMBO_API_KEY!}
userKey="user_123"
components={components}
resources={[{ uri: "mydb://products", name: "Product Catalog" }]}
listResources={async (query) => {
return [{ uri: "mydb://products/123", name: "Product #123" }];
}}
getResource={async (uri) => {
const res = await fetch(`/api/resource?uri=${encodeURIComponent(uri)}`);
return await res.json();
}}
>
<ChatInterface />
</TamboProvider>
The SDK exposes useTamboMcpServerInfos to list configured MCP servers and useTamboMcpResource for fetching resources. These are managed by TamboMcpProvider and TamboMcpTokenProvider within the provider hierarchy.
Key Source Files and Implementation Details
Understanding the internal architecture helps when extending the SDK or debugging streaming issues:
| Purpose | File Path |
|---|---|
| Main SDK entry point | react-sdk/src/v1/providers/tambo-v1-provider.tsx |
| Component & tool registry | react-sdk/src/providers/tambo-registry-provider.tsx |
| Primary hook (combined context) | react-sdk/src/v1/hooks/use-tambo-v1.ts |
| Component renderer (streams props, validates schema) | react-sdk/src/v1/components/v1-component-renderer.tsx |
| Thread-input hook (user message UI) | react-sdk/src/v1/hooks/use-tambo-v1-thread-input.ts |
| Stream-state context (per-thread event handling) | react-sdk/src/v1/providers/tambo-v1-stream-context.tsx |
| Utility: validate component props with Zod | react-sdk/src/util/registry.ts |
| MCP utilities (optional) | react-sdk/src/mcp/* |
The registry validation in validateAndPrepareComponent automatically attaches Zod schemas using the ~standard protocol, enabling runtime type safety for AI-generated props.
Summary
- TamboProvider initializes the SDK hierarchy, requiring
apiKeyanduserKeyfor authentication and thread ownership. - Component registration requires a React component, Zod schema (
propsSchema), and metadata; validation occurs inreact-sdk/src/util/registry.ts. - useTambo provides thread state, streaming status, and rendered components via
componentCacheReffor stable identities. - useTamboThreadInput handles user message submission with pending states.
- ComponentRenderer validates streaming props against Zod schemas and wraps components with context providers.
- MCP integration enables external tool access through
resources,listResources, andgetResourceprops onTamboProvider.
Frequently Asked Questions
How does Tambo validate props for AI-generated components?
Tambo validates props using Zod schemas attached during component registration. When the LLM streams a component block, the ComponentRenderer (in react-sdk/src/v1/components/v1-component-renderer.tsx) calls the ~standard validation protocol on the registered component's schema. If validation fails, the component does not render, preventing runtime errors from malformed AI output.
What is the difference between useTambo and useTamboThreadInput?
useTambo (defined in react-sdk/src/v1/hooks/use-tambo-v1.ts) provides access to the complete thread state, including message history, streaming status, rendered components, and thread management actions like startNewThread. useTamboThreadInput (in react-sdk/src/v1/hooks/use-tambo-v1-thread-input.ts) specifically manages the user input field state and submission handling, providing value, setValue, submit, and isPending for form UI construction.
Can I register components dynamically after the initial render?
Yes, you can register components dynamically using the useTamboRegistry hook, which exposes registerComponent and registerTool functions. These functions call the validation utilities in react-sdk/src/util/registry.ts to attach schemas and metadata to the internal componentList map. This enables runtime extension of the AI's available UI components without reloading the application.
How does Tambo handle streaming component props?
Tambo maintains component identity across streaming updates using a componentCacheRef inside useTambo (react-sdk/src/v1/hooks/use-tambo-v1.ts). As the LLM streams partial JSON for component props, the SDK updates the cached component's props while preserving the React component instance. The ComponentRenderer then receives these streaming props, validates them against the Zod schema, and re-renders the component with new data without unmounting it.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →