A2UI Best Practices: 10 Essential Patterns for Production Agent-to-UI Integration

A2UI best practices center on using the framework-agnostic @a2ui/web_core library for JSON-L stream processing, maintaining an adjacency-list component model for incremental updates, and strictly separating UI structure from application state via JSON-Pointer data binding.

A2UI (Agent-to-UI) is an open-source protocol from the google/A2UI repository that enables AI agents to stream declarative JSON messages to client renderers, building native user interfaces dynamically. Following these architectural patterns ensures your implementation remains reactive, secure, and framework-agnostic across React, Lit, or custom renderers.

Protocol Versioning and Catalog Setup

Select the Appropriate Specification Version

Start by pinning your implementation to a stable protocol version. v0.8 is the current production-ready specification and should be used unless you require v0.9’s catalog-aware surface creation features. The v0.9 specification is still in draft and introduces breaking changes to how component catalogs are declared during surface initialization.

Consult the version matrix in docs/concepts/overview.md and the formal specifications in docs/specification/v0.8-a2ui.md (stable) and docs/specification/v0.9-a2ui.md (draft) to determine compatibility with your agent’s output format.

Register the Component Catalog Before Processing

The component catalog declares the set of UI component types your client understands. Load the standard catalog immediately after application initialization to ensure the MessageProcessor can validate incoming component definitions.

For React applications, invoke initializeDefaultCatalog() once at startup. For custom implementations, extend the registry before calling processMessages():

import { ComponentRegistry } from '@a2ui/react';
import MyFancyCard from './components/MyFancyCard';

const registry = ComponentRegistry.getInstance();

registry.register('Card', {
  component: MyFancyCard,   // overrides the default Card
});

See the Component Registry section in renderers/react/README.md (lines 65-78) for extension patterns.

Core Architectural Patterns

Leverage the Framework-Agnostic web_core Library

All web renderers share a single battle-tested core that handles JSON-L streaming, schema validation, and state management. Import from @a2ui/web_core (or @a2ui/web_core/v0_9 for draft specification support) regardless of whether you use React, Lit, or Angular.

The core provides essential modules:

  • MessageProcessor: Parses and validates incoming JSON-L streams
  • SurfaceModel: Manages the adjacency-list component tree and root references
  • DataModel: Maintains the separate JSON state tree
  • ComponentModel: Handles property resolution and expression parsing

Reference the implementation in renderers/web_core/src/v0_9/processing/message-processor.ts and the interface definitions in renderers/web_core/src/v0_8/data/model-processor.ts.

Adopt the Adjacency-List Component Model

A2UI represents UI hierarchies as a flat list of component objects (id, component, properties) rather than nested trees. This adjacency-list pattern enables incremental updates—agents can append, remove, or modify single components without transmitting the entire UI structure on every change.

Store components in a Map<string, Component> indexed by ID within each SurfaceModel. Resolve parent-child relationships only at render time using the children property references. This design rationale is detailed in the Component Structure section of docs/concepts/overview.md (lines 18-20).

Decouple UI Structure from Application State

Maintain strict separation between UI components and data state. UI components reference the data model via JSON-Pointer paths (e.g., /users/0/name) rather than embedding values directly. Update the data tree independently using dataModelUpdate (v0.8) or updateDataModel (v0.9) messages.

This stateless approach allows the same UI definition to be reused across different datasets and ensures the renderer updates only when data actually changes. Binding rules are documented in docs/concepts/data-binding.md.

Framework-Specific Implementation

Implement Two-Context State Management (React)

React renderers should separate actions (stable callback references) from state (versioned data models) into two distinct contexts. This prevents React re-renders when only action handlers change but the underlying data remains constant.

The pattern is illustrated in the React architecture diagram in renderers/react/README.md (lines 84-99), showing how A2UIProvider maintains action stability while SurfaceModel handles data versioning.

Inject Base Styles Once and Theme Consistently

Call injectStyles() once during application initialization to load A2UI’s base CSS. Then apply a specific theme (e.g., litTheme or a custom React theme) by overriding only the necessary CSS variables or classes. Avoid inline style hacks that bypass the theming system.

Theme customization guidance is located in the Theme System section of renderers/react/README.md (lines 9-38) and the context provider in renderers/react/src/theme/ThemeContext.tsx.

Production Reliability and Security

Validate Schema Compatibility at Build Time

Run the built-in schema verification tests to ensure your renderer’s TypeScript types match the JSON schema shipped with the specification. This catches mismatched property names or invalid component definitions before production deployment.

Execute the test suites in renderers/web_core/src/v0_8/schema/verify-schema.test.ts and renderers/web_core/src/v0_9/schema/verify-schema.test.ts as part of your CI pipeline.

Handle Transport Errors and Ordering Guarantees

A2UI assumes ordered message delivery (typically via SSE, WebSockets, or A2A). Implement reconnection logic that either replays missed messages or restarts the surface entirely when the connection drops. The transport layer must guarantee that createSurface precedes updateComponents and that data model updates arrive in sequence.

Required client-side error handling strategies are outlined in docs/concepts/data-flow.md.

Secure the Agent-Client Boundary

Never trust data sent from the agent without validation—the web_core library handles schema validation automatically, but you must ensure the client never executes arbitrary code. Keep API keys (e.g., Gemini) out of the client bundle; use a backend proxy to authenticate with agent services, as demonstrated in the Quickstart guide.

Review the Security Notice in docs/quickstart.md (lines 22-25) for additional hardening recommendations.

Practical Implementation Examples

Basic React Setup (v0.8)

// src/App.tsx
import { injectStyles } from '@a2ui/react/styles';
import { A2UIProvider, A2UIRenderer, useA2UI } from '@a2ui/react';
import { initializeDefaultCatalog } from '@a2ui/react';

// One‑time initialization
injectStyles();
initializeDefaultCatalog();

function App() {
  const { processMessages } = useA2UI();

  // Example: fetch A2UI messages from a backend endpoint
  const loadUI = async () => {
    const resp = await fetch('/api/a2ui');
    const msgs = await resp.json();               // server‑to‑client messages
    processMessages(msgs);                         // feed to MessageProcessor
  };

  return (
    <A2UIProvider onAction={(e) => console.log('action', e)}>
      <button onClick={loadUI}>Load UI</button>
      {/* Render the surface named "main" */}
      <A2UIRenderer surfaceId="main" />
    </A2UIProvider>
  );
}

Reference: React provider implementation in renderers/react/README.md (lines 30-49).

Framework-Agnostic web_core Usage

import {
  MessageProcessor,
  SurfaceModel,
} from '@a2ui/web_core/v0_9';

// Create a processor that will own surfaces
const processor = new MessageProcessor();

// Feed a JSONL stream (e.g., from SSE)
processor.processLine('{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/basic_catalog.json"}}');
processor.processLine('{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{ "id":"title","component":"Text","text":"Hello A2UI"}]}}');
processor.processLine('{"version":"v0.9","updateDataModel":{"surfaceId":"main","path":"/","value":{"greeting":"Hello"}}');

// Later, retrieve the rendered model for any UI framework
const surface: SurfaceModel = processor.getSurface('main');
console.log(surface.rootComponentId); // → "title"

Reference: renderers/web_core/src/v0_9/processing/message-processor.ts.

Incremental Updates via Adjacency List

// Initial surface definition
processor.processLine('{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"list","component":"List","children":{"template":{"componentId":"item","dataBinding":"$path/to/items"}}]}');

// Later, add a new item without resending the whole list
processor.processLine('{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"item-3","component":"Text","text":"New Item"}]}');

The adjacency-list model lets you append or replace a single component; the renderer resolves the new tree automatically without requiring a full state refresh.

Summary

  • Use v0.8 for production stability or v0.9 for catalog-aware surface creation.
  • Build on @a2ui/web_core to share JSON-L processing logic across React, Lit, or Angular.
  • Maintain adjacency-list component structures in SurfaceModel to enable efficient incremental updates.
  • Separate UI components from data state, binding values via JSON-Pointer paths.
  • Register component catalogs before processing messages to ensure validation passes.
  • Implement two-context state management in React to optimize rendering performance.
  • Validate schema compatibility using the built-in test suites in renderers/web_core/src/v0_*/schema/.
  • Handle ordered transport with reconnection logic for SSE or WebSocket streams.
  • Keep API keys server-side and validate all agent inputs through the core schema validator.

Frequently Asked Questions

What is the difference between A2UI v0.8 and v0.9?

v0.8 is the stable, production-ready specification recommended for most applications. v0.9 is a draft specification that introduces catalog-aware surface creation and renames certain message types (e.g., updateDataModel instead of dataModelUpdate). Both versions are supported by @a2ui/web_core, but v0.8 offers long-term stability while v0.9 is subject to breaking changes.

Can I use A2UI without React?

Yes. The @a2ui/web_core library is framework-agnostic and provides MessageProcessor, SurfaceModel, and schema validation for any JavaScript renderer. The React, Lit, and Angular implementations in the repository all consume this shared core, as documented in docs/guides/renderer-development.md.

How does A2UI handle real-time UI updates efficiently?

A2UI uses an adjacency-list component model where the UI hierarchy is stored as a flat Map<string, Component> rather than a nested tree. This allows agents to send incremental updateComponents messages that add, remove, or modify single components without resending the entire UI structure, minimizing bandwidth and render cycles.

Where should I store API keys when using A2UI?

Never bundle API keys (such as Gemini API keys) in client-side code. Instead, use a backend proxy to handle agent authentication and message streaming, passing only validated A2UI JSON messages to the client. The Quickstart guide at docs/quickstart.md demonstrates this security pattern.

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 →