2D UI
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)#
If you only need to fetch raw data rows from a spreadsheet and don't need the automated Option selection state, use useAirDataset.
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
MediaRegistryand specific row interfaces fromsrc/registry/media.generated.ts.
Event & Payload Strictness (useAirEvent)#
Use events for transient 'fire and forget' actions. Always prefer useAirEvent to broadcast commands to the engine.
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)#
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.
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
useAirStoreoruseAirDatasetmanually for Option Models. - DO NOT pass a
storeRootstring. Pass the entireoptionSetobject from the registry.
Store Integration (useAirStore)#
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>();
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>
);
}patch({ ...state, val: 1 })). This captures stale closures and overwrites engine calculations. Let the Vault deep-merge it.React UI Implementation Template#
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>
);
}





