React Integration Patterns (Dispense SDK)

The Dispense SDK provides comprehensive React integration through contexts and hooks that make KOS models easily accessible in React components with automatic reactivity. This integration layer wraps the underlying model system and ensures components automatically re-render when model state changes.

Context System

The SDK provides dedicated React contexts for both hardware and consumer UI hierarchies. These contexts expose models to child components and handle model lifecycle automatically.

Hardware Hierarchy Contexts

For crew and maintenance applications working with hardware components:

AssemblyProvider and useAssemblyContext

Provides access to the AssemblyModel singleton representing the device’s hardware topology.

import { AssemblyProvider, useAssemblyContext } from '@kosdev-code/kos-dispense-sdk';
function App() {
  return (
    <AssemblyProvider>
      <HardwareView />
    </AssemblyProvider>
  );
}
function HardwareView() {
  const { model: assembly } = useAssemblyContext();
  // Access hardware topology
  const holders = assembly?.holderContainer.holders;
  const pumps = assembly?.pumpContainer.models;
  const nozzlePaths = assembly?.nozzlePaths;
  return (
    <div>
      <h2>Device Nozzles</h2>
      {nozzlePaths?.map(path => (
        <div key={path}>{path}</div>
      ))}
    </div>
  );
}

The AssemblyProvider automatically loads the assembly topology during initialization. The context provides:

  • model – The AssemblyModel instance (available after loading)
  • Type parameters for custom assembly and holder extensions

Consumer UI Hierarchy Contexts

For consumer-facing applications built around beverage selection:

DeviceAssemblyProvider and useDeviceAssemblyContext

Provides access to the DeviceAssemblyModel singleton, the entry point for consumer UI applications.

import { DeviceAssemblyProvider, useDeviceAssemblyContext } from '@kosdev-code/kos-dispense-sdk';
function App() {
  return (
    <DeviceAssemblyProvider>
      <ConsumerUI />
    </DeviceAssemblyProvider>
  );
}
function ConsumerUI() {
  const { model: deviceAssembly, nozzlePaths } = useDeviceAssemblyContext();
  return (
    <div>
      {nozzlePaths?.map(path => (
        <NozzleView key={path} path={path} />
      ))}
    </div>
  );
}

The DeviceAssemblyProvider automatically loads nozzle information from the device topology. Children are only rendered after the model is ready. The context provides:

  • model – The DeviceAssemblyModel instance
  • nozzlePaths – Array of available nozzle paths for convenience

NozzleProvider and useNozzleContext

Provides access to a specific NozzleModel representing a dispense point with selection and pour capabilities.

import { NozzleProvider, useNozzleContext } from '@kosdev-code/kos-dispense-sdk';
function NozzleView({ path }: { path: string }) {
  return (
    <NozzleProvider path={path}>
      <BeverageSelection />
      <PourButton />
    </NozzleProvider>
  );
}
function BeverageSelection() {
  const { model: nozzle } = useNozzleContext();
  const brands = nozzle?.visibleBrands;
  const selectedBrand = nozzle?.selectedBrand;
  return (
    <div>
      {brands?.map(brand => (
        <button
          key={brand.id}
          onClick={() => nozzle?.setSelectedBrand(brand)}
          className={brand === selectedBrand ? 'selected' : ''}
        >
          {brand.data.name}
        </button>
      ))}
    </div>
  );
}

The NozzleProvider requires a path prop identifying which nozzle to load. The context provides:

  • model – The NozzleModel instance for the specified path

Context Hierarchy Patterns

Contexts can be nested to provide layered access. For consumer applications, the typical hierarchy is:

<DeviceAssemblyProvider>
  <NozzleProvider path="assembly:core:nozzle:nozzle">
    <BeverageSelectionUI />
  </NozzleProvider>
</DeviceAssemblyProvider>

For hardware applications accessing specific components:

<AssemblyProvider>
  <HardwareManagementScreen />
</AssemblyProvider>

Component Hooks

The SDK provides hooks for accessing individual model instances within components. These hooks use the KOS framework’s useKosModel internally and provide automatic reactivity.

Important Performance Note: In most cases, you should pass models as props rather than retrieving them via hooks. Models accessed through contexts (like assembly.holderContainer.holders) can be passed directly to child components. The individual component hooks (useHolder, usePump, useBoard) are primarily escape hatches for scenarios where direct model access is needed outside of normal context flow. Using these hooks extensively (e.g., 50+ components each calling useHolder) can create performance bottlenecks due to the suspense framework’s model retrieval mechanism.

Hardware Component Hooks

useHolder

Access a specific HolderModel by path. Use sparingly – prefer passing holder models as props from parent contexts.

import { useHolder } from '@kosdev-code/kos-dispense-sdk';
// ESCAPE HATCH PATTERN - use when you need a specific holder outside normal flow
function IsolatedHolderDetails({ path }: { path: string }) {
  const { model: holder, ready } = useHolder(path);
  if (!ready || !holder) return <div>Loading...</div>;
  return (
    <div>
      <h3>{holder.name}</h3>
      <p>Ingredient: {holder.ingredientName || 'Unassigned'}</p>
      <p>Fill Level: {holder.fillPercent}%</p>
    </div>
  );
}
// PREFERRED PATTERN - pass models as props
function HolderDetails({ holder }: { holder: HolderModel }) {
  return (
    <div>
      <h3>{holder.name}</h3>
      <p>Ingredient: {holder.ingredientName || 'Unassigned'}</p>
      <p>Fill Level: {holder.fillPercent}%</p>
      {holder.troubles.length > 0 && (
        <div className="warnings">
          {holder.troubles.map(trouble => (
            <div key={trouble.id}>{trouble.data.message}</div>
          ))}
        </div>
      )}
    </div>
  );
}

Returns:

  • model – The HolderModel instance
  • ready – Boolean indicating if the model is loaded and ready

When to use:

  • Accessing a specific holder outside of AssemblyContext flow
  • Building utility components that need a single holder reference
  • Testing or debugging scenarios

When NOT to use:

  • Inside components that already have access to holders via assembly.holderContainer.holders
  • When iterating over multiple holders – pass models as props instead

usePump

Access a PumpModel for direct pump control. Use sparingly – prefer accessing pumps via their parent holder (holder.pumps).

import { usePump } from '@kosdev-code/kos-dispense-sdk';
// ESCAPE HATCH PATTERN - rarely needed
function IsolatedPumpControl({ pumpId }: { pumpId: string }) {
  const { model: pump, ready } = usePump();
  const handlePour = async () => {
    if (pump?.canPour) {
      await pump.performIntent({ intent: 'small' }, tracker);
    }
  };
  return (
    <button onClick={handlePour} disabled={!pump?.canPour}>
      {pump?.isPouring ? 'Pouring...' : 'Pour Small'}
    </button>
  );
}
// PREFERRED PATTERN - access pumps via holder context
function PumpControl({ holder }: { holder: HolderModel }) {
  const pump = holder.pumps.models[0]; // or iterate holder.pumps.models
  const handlePour = async () => {
    if (pump?.canPour) {
      await pump.performIntent({ intent: 'small' }, tracker);
    }
  };
  return (
    <button onClick={handlePour} disabled={!pump?.canPour}>
      {pump?.isPouring ? 'Pouring...' : 'Pour Small'}
    </button>
  );
}

Returns:

  • model – The PumpModel instance
  • ready – Boolean indicating if the model is loaded and ready

When to use:

  • Direct pump access outside holder context
  • Specialized pump diagnostic tools

When NOT to use:

  • When you already have a holder model with holder.pumps access
  • When building holder-based UI components

useBoard

Access a BoardModel by path.

import { useBoard } from '@kosdev-code/kos-dispense-sdk';
function BoardInfo({ path }: { path: string }) {
  const { model: board, ready } = useBoard(path);
  if (!ready || !board) return null;
  return (
    <div>
      <h4>Board: {board.name}</h4>
      <p>Path: {board.path}</p>
    </div>
  );
}

Returns:

  • model – The BoardModel instance
  • ready – Boolean indicating if the model is loaded and ready

Consumer UI Hooks

useAvailability

Access an AvailabilityModel representing a selectable item (brand, beverage, or flavor).

import { useAvailability } from '@kosdev-code/kos-dispense-sdk';
function AvailabilityCard() {
  const { model: availability, ready } = useAvailability();
  if (!ready || !availability) return null;
  return (
    <div className={!availability.available ? 'unavailable' : ''}>
      <h4>{availability.data.name}</h4>
      <p>Available: {availability.available ? 'Yes' : 'No'}</p>
    </div>
  );
}

Returns:

  • model – The AvailabilityModel instance
  • ready – Boolean indicating if the model is loaded and ready

Consumer UI Kit

For building consumer-facing selection and pour interfaces, the SDK provides the Consumer UI Kit (CUI Kit) – a comprehensive set of hooks and patterns that encapsulate best practices for brand-beverage-flavor selection workflows.

The CUI Kit is the recommended approach for consumer UIs. Rather than working with individual hooks and context patterns, the CUI Kit provides:

  • useCuiKit – Complete selection workflow management (brands → beverages → flavors)
  • Integrated pour operations – Built-in pour handling with validation hooks
  • Navigation state – Automatic back/home navigation between selection levels
  • Selection tracking – Coordinated state management across all selection levels
  • EventBus integration – Automatic analytics events for all user interactions
  • Real-time list updates – Beverage list change notifications for animations and analytics
  • Timer management – Lifecycle control for ambient/attract mode timers

The CUI Kit uses the underlying hooks and contexts documented in this guide (useNozzleContext, useNozzlePour, etc.) to provide a streamlined developer experience that captures common consumer UI patterns.

See Consumer UI Kit for complete CUI Kit documentation including:

  • Selection workflow patterns
  • Pour integration and validation
  • Companion model support
  • Complete working examples

If you’re building a consumer-facing beverage selection interface, start with the CUI Kit rather than assembling the lower-level hooks yourself.

Reactive Updates with kosAutoEffect

All SDK hooks leverage the KOS framework’s reactivity system. Components automatically re-render when observed model properties change, thanks to the underlying kosAutoEffect integration in useKosModel.

This means:

function PourStatus({ pump }: { pump: PumpModel }) {
  // This component automatically re-renders when:
  // - pump.isPouring changes
  // - pump.pouringState changes
  // - pump.currentState changes
  return (
    <div>
      <p>State: {pump.pouringState}</p>
      <p>Pouring: {pump.isPouring ? 'Yes' : 'No'}</p>
    </div>
  );
}

No manual subscriptions or useEffect hooks are needed – the component observes the model properties it accesses and updates automatically.

Performance Considerations

Context Placement

Place contexts at the appropriate level in your component tree:

  • AssemblyProvider / DeviceAssemblyProvider – At application root
  • NozzleProvider – Per nozzle/dispense point
  • Avoid nesting contexts unnecessarily

Prefer Props Over Hooks

Best Practice: Pass models as props rather than using component hooks repeatedly:

// PREFERRED - pass models as props
function HolderList() {
  const { model: assembly } = useAssemblyContext();
  const holders = assembly?.holderContainer.holders || [];
  return (
    <div>
      {holders.map(holder => (
        <HolderCard key={holder.path} holder={holder} />
      ))}
    </div>
  );
}
function HolderCard({ holder }: { holder: HolderModel }) {
  // Direct model access via props - no hook needed
  return <div>{holder.name}: {holder.ingredientName}</div>;
}
// AVOID - multiple hook calls create suspense overhead
function HolderList() {
  const { model: assembly } = useAssemblyContext();
  const holderPaths = assembly?.holderContainer.holders.map(h => h.path) || [];
  return (
    <div>
      {holderPaths.map(path => (
        <HolderCard key={path} path={path} />
      ))}
    </div>
  );
}
function HolderCard({ path }: { path: string }) {
  // Each component calls useHolder - creates suspense overhead
  const { model: holder } = useHolder(path);
  return <div>{holder?.name}: {holder?.ingredientName}</div>;
}

Model Instance Reuse

The KOS framework’s model registry ensures models are shared across components:

// Both components access the SAME HolderModel instance
function Component1() {
  const { model } = useHolder('holder-A1');
  // ...
}
function Component2() {
  const { model } = useHolder('holder-A1');
  // ...
}

This sharing enables automatic synchronization across all components observing the same model.

Example: Building a Hardware Management Screen

Combining contexts and passing models as props for optimal performance:

import {
  AssemblyProvider,
  useAssemblyContext,
  type HolderModel
} from '@kosdev-code/kos-dispense-sdk';
function App() {
  return (
    <AssemblyProvider>
      <HardwareManagementScreen />
    </AssemblyProvider>
  );
}
function HardwareManagementScreen() {
  const { model: assembly } = useAssemblyContext();
  const holders = assembly?.holderContainer.holders || [];
  return (
    <div className="hardware-screen">
      <h1>Ingredient Management</h1>
      <div className="holder-grid">
        {holders.map(holder => (
          <HolderCard key={holder.path} holder={holder} />
        ))}
      </div>
    </div>
  );
}
function HolderCard({ holder }: { holder: HolderModel }) {
  return (
    <div className="holder-card">
      <h3>{holder.name}</h3>
      <div className="status">
        {holder.ingredientName ? (
          <>
            <p>{holder.ingredientName}</p>
            <p>{holder.fillPercent}% remaining</p>
          </>
        ) : (
          <p>Unassigned</p>
        )}
      </div>
      {holder.troubles.length > 0 && (
        <div className="troubles">
          {holder.troubles.map(t => (
            <span key={t.id} className="trouble-badge">
              {t.data.message}
            </span>
          ))}
        </div>
      )}
    </div>
  );
}

Next Steps


Note: All APIs referenced in this document have been validated against the actual SDK source code in /packages/sdk/kos-dispense-sdk/src/ui/.