How to Implement Real-Time AI-Managed State in React Components with Tambo

Tambo's React SDK enables real-time AI-managed state by streaming AG-UI events through a reducer that accumulates component props, allowing LLM-generated updates to render instantly in registered React components without manual state synchronization.

The tambo-ai/tambo repository provides a React SDK that bridges large language model outputs directly to your UI components. By implementing real-time AI-managed state in React components with Tambo, you can create dynamic interfaces where the AI progressively constructs or modifies visual elements through a streaming event architecture.

Understanding the Event Streaming Architecture

AG-UI Events and the Stream Context

The SDK implements an AG-UI (Agent-User Interface) protocol where useTambo establishes a stream context that receives low-level events from the Tambo AI server. These events include tambo.component.start, tambo.component.props_delta, and tambo.component.end, each representing a distinct phase in the component lifecycle.

The streamReducer and Event Accumulation

Located in react-sdk/src/v1/utils/event-accumulator.ts, the streamReducer function translates incoming events into immutable thread state:

  • tambo.component.start creates a component content block with streamingState: "started"
  • tambo.component.props_delta applies a JSON Patch to the component's props and sets streamingState: "streaming"
  • tambo.component.end finalizes the block with streamingState: "done"

The reducer guarantees immutability by returning fresh ThreadState objects for every event, preventing side effects and enabling predictable state transitions.

Registering Components for AI Control

Before the LLM can manage component state, you must register React components with the SDK. The registerComponent function (utilized via react-sdk/src/v1/providers/tambo-registry-provider.tsx) maps component names to React components and optional Zod-style validation schemas.

import { registerComponent } from '@tambo-ai/react';
import MyChart from '@/components/MyChart';
import { z } from 'zod';

registerComponent('my_chart', {
  component: MyChart,
  props: {
    '~standard': z.object({
      data: z.array(z.number()),
      title: z.string(),
    }),
  },
});

Consuming the AI-Managed State

The useTambo Hook

The useTambo hook (defined in react-sdk/src/v1/hooks/use-tambo-v1.ts) memoizes the thread state, messages, and component registry. It returns isStreaming to indicate active LLM communication and provides access to the accumulated thread state. The hook also handles PLACEHOLDER_THREAD_ID for optimistic UI updates before the API returns a real thread ID.

import { useTambo } from '@tambo-ai/react';

export function Chat() {
  const {
    messages,
    isStreaming,
    registerComponent,
  } = useTambo('my_thread');

  return (
    <div className="space-y-4">
      {messages.map((msg) => (
        <Message key={msg.id} message={msg} />
      ))}
      {isStreaming && <LoadingSpinner />}
    </div>
  );
}

Rendering Components with ComponentRenderer

The ComponentRenderer (implemented in react-sdk/src/v1/components/v1-component-renderer.tsx) reads component content blocks and handles partial JSON parsing using the partial-json library. It validates props against registered schemas and renders the actual React element.

import { ComponentRenderer } from '@tambo-ai/react';

function Message({ message }) {
  return (
    <div className="p-2 bg-gray-50 rounded">
      {message.content.map((c) => {
        if (c.type === 'component') {
          return (
            <ComponentRenderer
              key={c.id}
              content={c}
              threadId={message.threadId}
              messageId={message.id}
              fallback={<div>Unknown component: {c.name}</div>}
            />
          );
        }
        return <p>{c.text}</p>;
      })}
    </div>
  );
}

Accessing Props with useTamboComponentState

Inside registered components, the useTamboComponentState hook accesses the AI-managed props. Because the ComponentContentProvider supplies context values including componentId, threadId, messageId, and componentName, and because the rendered element is memoized on content.id, React preserves the component instance across updates. This enables in-place state updates as the LLM streams new props via tambo.component.props_delta events.

import { useTamboComponentState } from '@tambo-ai/react';

export function MyChart() {
  const { props } = useTamboComponentState();
  // Props update in-place as TOOL_CALL_ARGS / COMPONENT_PROPS_DELTA events arrive
  return <Chart data={props.data} title={props.title} />;
}

How the SDK Maintains Real-Time Synchronization

Immutability and Thread State

The streamReducer in react-sdk/src/v1/utils/event-accumulator.ts guarantees immutability by returning fresh ThreadState objects for every event. This prevents side effects and enables predictable state transitions. The system uses UnreachableCaseError for exhaustive type checking on unknown event types during development, ensuring all AG-UI events are handled correctly.

In-Place Updates and Memoization

The v1-component-renderer.tsx uses React's memoization on content.id to ensure that as props_delta events arrive, the same component instance receives updated props rather than unmounting and remounting. This is critical for maintaining internal React state (like form inputs or animation states) while the AI progressively updates the component's configuration. The wire-up between the client and reducer occurs in react-sdk/src/v1/utils/stream-handler.ts, which manages the React useReducer and useEffect integration.

Summary

  • Tambo's React SDK bridges LLM outputs to React components through an AG-UI event streaming protocol implemented in react-sdk/src/v1/utils/stream-handler.ts.
  • Event accumulation in react-sdk/src/v1/utils/event-accumulator.ts translates tambo.component.* events into immutable thread state with streamingState tracking ("started", "streaming", "done").
  • Component registration via registerComponent maps AI-referenced names to React components with optional Zod validation, managed by tambo-registry-provider.tsx.
  • Real-time rendering uses ComponentRenderer to handle partial JSON parsing and schema validation, while useTambo and useTamboComponentState provide access to AI-managed state.
  • In-place updates are achieved through memoization on content.id in v1-component-renderer.tsx, allowing React to preserve component instances as the LLM streams props_delta events.

Frequently Asked Questions

What types of components can I register with Tambo?

You can register any React component that accepts props. The SDK supports Zod-style validation schemas for type safety, and components can range from simple UI elements like buttons and cards to complex interactive widgets like charts, forms, and maps. The only requirement is that the component must be registered with a unique name that the LLM can reference when emitting tambo.component.start events.

How does Tambo handle partial or incomplete JSON during streaming?

The ComponentRenderer in react-sdk/src/v1/components/v1-component-renderer.tsx uses the partial-json library to parse incoming props during the streaming phase. This allows the component to render with partial data as soon as the first tambo.component.props_delta event arrives, rather than waiting for the complete JSON payload. As subsequent delta events arrive, the props are patched in-place, creating a progressive rendering effect.

Can I use Tambo with Next.js App Router?

Yes, Tambo's React SDK is compatible with Next.js App Router. The hooks (useTambo, useTamboComponentState) and providers work within client components. You should wrap your application or specific routes with the Tambo provider in a client component boundary (using the "use client" directive), then use the hooks in child components to access the AI-managed state. The SDK handles thread management and streaming entirely on the client side via react-sdk/src/v1/utils/stream-handler.ts.

What happens if the LLM emits an unknown component name?

If the LLM emits a tambo.component.start event with a component name that hasn't been registered via registerComponent, the ComponentRenderer will render the fallback prop provided to it. This allows you to display a graceful error message or placeholder UI when the AI attempts to use a component that doesn't exist in your registry. The SDK also performs exhaustive type checking with UnreachableCaseError for unknown event types during development to ensure all AG-UI events are handled correctly.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →