2D UI

React UI overlay architecture for Aircada web apps, utilizing state synchronization and viewport interaction properties.
Published: 7/10/2026

In the Aircada SDK architecture, the 2D React UI layer acts as an optimistic view reflecting and triggering state in the global Twin-Proxy Vault.

React overlays observe state updates and patch adjustments back to the Vault, allowing high-frequency rendering and 3D engine physics loops to execute smoothly without blocking UI elements.

Raw Data Fetching (useAirDataset)#

Guidelines for accessing configuration data, lists, and menus from Aircada Cloud Datasets.

If you only need to fetch raw data rows from a spreadsheet and don't need the automated Option selection state, use useAirDataset.

tsx
import { useAirDataset } from '@aircada/air-react';
import { MediaRegistry, MenuDataRow } from '../registry/media.generated';

export function SidebarMenu() {
    const { rows, isLoading } = useAirDataset<MenuDataRow>(
        MediaRegistry.DATASETS.MAIN_MENU.id
    );

    if (isLoading) return <div>Loading...</div>;

    return (
        <ul>
            {rows.map(row => (
                <li key={row.id}>{row.Label}</li>
            ))}
        </ul>
    );
}
  • Single Source of Truth: Never hardcode lists or configuration options.
  • The Hook: Always use useAirDataset<RowInterface>(MediaRegistry.DATASETS.TARGET_DATASET.id) to fetch data.
  • Typing: Import MediaRegistry and specific row interfaces from src/registry/media.generated.ts.

Event & Payload Strictness (useAirEvent)#

Rules for triggering explicit actions or commands via the global event bus.

Use events for transient 'fire and forget' actions. Always prefer useAirEvent to broadcast commands to the engine.

tsx
import { useAirEvent } from '@aircada/air-react';
import { Events, EventPayloads } from '../registry/registry.generated';

export function ResetButton() {
    const resetCameraEvent = useAirEvent<EventPayloads[typeof Events.RESET_CAMERA]>(Events.RESET_CAMERA);

    const handleReset = () => {
        resetCameraEvent.invoke({ reason: 'User requested reset' });
    };

    return <button onClick={handleReset}>Reset Camera</button>;
}
  • No Magic Strings: All event names must be imported from src/registry/registry.generated.ts.
  • Strict Typing: All transmitters and receivers must be strongly typed using EventPayloads[typeof Events.YOUR_EVENT].

The Options Pipeline (useAirOptions)#

Using the useAirOptions hook for product configurator UI components.

When your UI needs to display choices from an Options Registry (like colors or materials), use useAirOptions. The hook acts as a pure, data-bound abstraction that handles fetching dataset rows, reading Vault state, and safely patching using deep-string paths.

tsx
import { useAirOptions } from '@aircada/air-react';
import { OptionsRegistry } from '../registry/options.generated';

export function ColorPicker() {
    const { options, isLoading, activeSelection, selectOption } = useAirOptions(
        OptionsRegistry.BALL_O_STEEL.sets.SIMPLECOLORS
    );

    if (isLoading) return <div>Loading...</div>;

    return (
        <div className="flex gap-2">
            {options.map(row => (
                <button
                    key={row.id}
                    className={activeSelection?.id === row.id ? 'border-cyan-500' : 'border-gray-500'}
                    onClick={() => selectOption(row)}
                >
                    {row.name}
                </button>
            ))}
        </div>
    );
}
  • DO NOT use useAirStore or useAirDataset manually for Option Models.
  • DO NOT pass a storeRoot string. Pass the entire optionSet object from the registry.

Store Integration (useAirStore)#

React guidelines and code examples for reading and writing to global Twin-Proxy state via the useAirStore hook.

Use the useAirStore hook to access state and trigger deep partial patches. The Twin-Proxy Vault handles deep-merging automatically.

API Syntax: const [storeState, patch] = useAirStore<AirStoreConfig>();

tsx
import { useAirStore } from '@aircada/air-react';
import { AirStoreConfig } from './store/store.config';

export function CartPanel() {
    const [storeState, patch] = useAirStore<AirStoreConfig>();

    const toggleCart = () => {
        // Deep partial patching
        patch({ ui: { cartOpen: !storeState.ui.cartOpen } });
    };

    return (
        <button onClick={toggleCart}>
            {storeState.ui.cartOpen ? 'Close Cart' : 'Open Cart'}
        </button>
    );
}
warning
STRICT PROHIBITION: NEVER spread existing state in a patch (e.g., patch({ ...state, val: 1 })). This captures stale closures and overwrites engine calculations. Let the Vault deep-merge it.

React UI Implementation Template#

The 'Gold Standard' pattern for generating or modifying React components in Aircada.
tsx
import React, { useState } from 'react';
import { AircadaViewport, useAirStore, useAirDataset } from '@aircada/react';
import { MediaRegistry, ConfiguratorRow } from './registry/media.generated';
import { AirStoreConfig } from './store/store.config';

export default function UIOverlay() {
    const [storeState, patch] = useAirStore<AirStoreConfig>();
    const [isMenuOpen, setIsMenuOpen] = useState(false); // Local state is fine

    const { rows, isLoading } = useAirDataset<ConfiguratorRow>(
        MediaRegistry.DATASETS.MAIN_CONFIGURATOR.id
    );

    const handleSelectOption = (rowId: string) => {
        patch({ configurator: { activeOptionId: rowId } });
    };

    return (
        <div className="aircada-root relative w-full h-screen overflow-hidden pointer-events-none">
            <AircadaViewport className="absolute inset-0 z-0" />
            <div className="absolute bottom-8 left-1/2 -translate-x-1/2 z-10 p-4 bg-black/50 pointer-events-auto">
                {isLoading ? <span>Loading...</span> : rows.map(row => (
                    <button key={row.id} onClick={() => handleSelectOption(row.id)}>
                        {row.OptionName}
                    </button>
                ))}
            </div>
        </div>
    );
}