# How Tambo AI Manages the Lifecycle of Dynamically Generated UI Elements

> Discover how Tambo AI manages dynamic UI elements with a registry-driven architecture. Learn about stable React keys and context-backed state maps for efficient lifecycle management.

- Repository: [tambo ai/tambo](https://github.com/tambo-ai/tambo)
- Tags: internals
- Published: 2026-02-16

---

**Tambo AI uses a registry-driven architecture that separates component registration, rendering, and state persistence to manage dynamically generated UI elements through stable React keys and context-backed state maps.**

Tambo AI generates user interfaces dynamically based on AI responses, requiring a robust system to handle creation, rendering, and state management. The open-source **tambo-ai/tambo** repository implements a **registry-driven architecture** to manage the lifecycle of dynamically generated UI elements, ensuring components maintain state across re-renders and streaming updates.

## The Three-Pillar Architecture for Dynamic UI Lifecycle

Tambo AI separates concerns into three distinct phases: registration, rendering, and state persistence. This separation allows the AI backend to reference components by name without importing them directly, while the frontend safely materializes and maintains these elements.

### Component Registration via TamboRegistryProvider

At the foundation of the lifecycle is the `TamboRegistryProvider`, which maintains a plain-object map called `componentList` keyed by component name. When an application boots, developers call the `registerComponent` API (or pass a `components` prop to the provider) to add React components to the registry at runtime.

The provider validates each definition through `validateAndPrepareComponent` before storage, checking for valid prop schemas and name uniqueness. This registration pattern decouples component definitions from their instantiation, allowing the AI backend to reference components by name without importing them directly.

*Source:* [`react-sdk/src/providers/tambo-registry-provider.tsx`](https://github.com/tambo-ai/tambo/blob/main/react-sdk/src/providers/tambo-registry-provider.tsx) (lines 52-78)

### Safe Rendering with ComponentRenderer

When the AI backend returns a message containing a component content block, the `ComponentRenderer` (implemented in [`v1-component-renderer.tsx`](https://github.com/tambo-ai/tambo/blob/main/v1-component-renderer.tsx)) orchestrates the instantiation. The renderer looks up the component name in `registry.componentList` via `getComponentFromRegistry`, validates supplied props against any attached JSON-Schema, and creates the React element using `React.createElement`.

Crucially, the renderer wraps each component in a `ComponentContentProvider`, which injects the surrounding message context (`threadId`, `componentId`, etc.) into the component tree. This wrapper enables child hooks to access lifecycle metadata without prop drilling.

*Source:* [`react-sdk/src/v1/components/v1-component-renderer.tsx`](https://github.com/tambo-ai/tambo/blob/main/react-sdk/src/v1/components/v1-component-renderer.tsx) (lines 78-146)

### State Persistence Through Scoped Hooks

To prevent state loss during re-renders or streaming updates, Tambo AI implements component-scoped state via the `useTamboComponentState` hook. This hook stores state in a `Map` maintained inside `TamboRegistryProvider`, using a composite key format: `${threadId}.${componentId}.${stateKey}`.

The `ComponentRenderer` memoizes the wrapper element with a stable `key={content.id}` derived from the content block's unique identifier. As long as the underlying content block ID remains unchanged, React preserves the component instance, and the state hook continues to resolve to the same Map entry. If the block ID changes, the old entry is garbage-collected through cleanup effects in `ComponentContentProvider`.

*Source:* [`react-sdk/src/v1/hooks/use-tambo-v1-component-state.tsx`](https://github.com/tambo-ai/tambo/blob/main/react-sdk/src/v1/hooks/use-tambo-v1-component-state.tsx)

## Step-by-Step Lifecycle of a Dynamic Component

The complete lifecycle follows these distinct phases:

1. **Registration** – At bootstrap, `registerComponent` validates and stores the component definition in `componentList` within `TamboRegistryProvider`.

2. **Message Arrival** – The AI backend streams a message containing a component block with `type: "component"`, `name`, and `props`.

3. **Registry Lookup** – `ComponentRenderer` calls `getComponentFromRegistry` to retrieve the component definition and validates props against the stored schema.

4. **Stable Instantiation** – The renderer creates a memoized wrapper with `key={content.id}`, ensuring React maintains instance stability across updates.

5. **State Attachment** – Inside the component, `useTamboComponentState` resolves state from the provider's Map using the composite key, persisting data across re-renders.

6. **Dynamic Updates** – If the AI streams updated props for the same component block (same `content.id`), the component re-renders with new props but retains existing state.

7. **Cleanup** – When the message is deleted or the component block ID changes, `ComponentContentProvider` effects remove the state entry from the Map, preventing memory leaks.

## Implementation Examples

### Registering Components at Bootstrap

Register components when initializing the Tambo provider to make them available for AI generation:

```tsx
import { TamboRegistryProvider } from "@tambo-ai/react";
import { WeatherWidget } from "./components/WeatherWidget";
import { StockChart } from "./components/StockChart";

function App() {
  return (
    <TamboRegistryProvider
      components={[
        {
          name: "WeatherWidget",
          description: "Displays current weather for a location",
          component: WeatherWidget,
          props: {
            location: { type: "string", description: "City name" },
          },
        },
        {
          name: "StockChart",
          description: "Renders stock price history",
          component: StockChart,
        },
      ]}
    >
      <ChatInterface />
    </TamboRegistryProvider>
  );
}

```

*Reference:* Registration logic in [`tambo-registry-provider.tsx`](https://github.com/tambo-ai/tambo/blob/main/tambo-registry-provider.tsx) (lines 71-78).

### Rendering AI-Generated Components

Use `ComponentRenderer` to materialize components from AI message content:

```tsx
import { ComponentRenderer } from "@tambo-ai/react";

function MessageBubble({ message }) {
  return (
    <div className="message">
      {message.content.map((block) => {
        if (block.type === "component") {
          return (
            <ComponentRenderer
              key={block.id}
              content={block}
              threadId={message.threadId}
              messageId={message.id}
              fallback={<div>Component "{block.name}" not found</div>}
            />
          );
        }
        return <TextBlock key={block.id} content={block.text} />;
      })}
    </div>
  );
}

```

*Reference:* Rendering flow in [`v1-component-renderer.tsx`](https://github.com/tambo-ai/tambo/blob/main/v1-component-renderer.tsx) (lines 80-90) and fallback handling (lines 41-44).

### Persisting Component State

Maintain local state across re-renders using the scoped state hook:

```tsx
import { useTamboComponentState } from "@tambo-ai/react";

export function WeatherWidget({ location }: { location: string }) {
  const [unit, setUnit] = useTamboComponentState("unit", "metric");
  const [isPinned, setIsPinned] = useTamboComponentState("isPinned", false);

  return (
    <div className={`weather-widget ${isPinned ? "pinned" : ""}`}>
      <h3>Weather in {location}</h3>
      <button onClick={() => setUnit(u => u === "metric" ? "imperial" : "metric")}>
        Switch to {unit === "metric" ? "°F" : "°C"}
      </button>
      <button onClick={() => setIsPinned(p => !p)}>
        {isPinned ? "Unpin" : "Pin"}
      </button>
    </div>
  );
}

```

*Reference:* State hook implementation in [`use-tambo-v1-component-state.tsx`](https://github.com/tambo-ai/tambo/blob/main/use-tambo-v1-component-state.tsx).

## Summary

- **Registry-driven architecture** separates component definitions from instantiation, enabling runtime registration via `TamboRegistryProvider` and `registerComponent`.
- **Stable rendering** uses memoized wrappers with `key={content.id}` in `ComponentRenderer` to preserve React instances across streaming updates.
- **Scoped state persistence** stores per-component data in a composite-keyed Map (`${threadId}.${componentId}.${stateKey}`) accessed via `useTamboComponentState`.
- **Automatic cleanup** removes state entries when component block IDs change or unmount, preventing memory leaks through `ComponentContentProvider` effects.

## Frequently Asked Questions

### How does Tambo AI prevent state loss during streaming updates?

Tambo AI prevents state loss by memoizing the `ComponentRenderer` wrapper with a stable `key={content.id}` derived from the content block's unique identifier. As long as the AI streams updates to the same component block without changing its ID, React preserves the component instance. Inside the component, `useTamboComponentState` stores data in a Map using a composite key of `threadId`, `componentId`, and state key, ensuring the same storage location is accessed across re-renders.

### What happens if a component name isn't found in the registry?

When `ComponentRenderer` attempts to render a component, it calls `getComponentFromRegistry` to look up the name in `registry.componentList`. If the component is not found, the renderer displays the `fallback` prop passed to it, which defaults to a simple "Unknown component" message. This prevents application crashes while allowing developers to provide custom error UIs for missing components defined in [`react-sdk/src/util/registry.ts`](https://github.com/tambo-ai/tambo/blob/main/react-sdk/src/util/registry.ts).

### Can components be unregistered dynamically?

Yes, the `TamboRegistryProvider` exposes an `unregisterComponent` function through its internal context, allowing dynamic removal of component definitions. This capability supports hot-reloading scenarios and test cleanup, though production applications typically rely on stable component IDs for lifecycle management rather than frequent registration and deregistration.

### How is component state isolated between different threads?

State isolation is achieved through the composite key structure used by `useTamboComponentState`. The hook constructs a unique identifier by combining `threadId`, `componentId`, and the developer-provided state key into the format `${threadId}.${componentId}.${stateKey}`. This ensures that components with the same name or state keys in different conversation threads access distinct entries in the provider's state Map, preventing data leakage between threads.