React Rendering Architecture

The renderer's design in one sentence: a transport-free core walks the {areas, data} UiControl tree, dispatching each control's $type against a swappable component registry — and everything live (subscriptions, patches, events) is hidden behind one small interface, AreaSource.

        layout-area stream  ({areas, data} UiControl tree + patches)
                    │
          ┌─────────┴─────────┐
          │  renderer core    │   dispatch on $type, pop skins, resolve bindings, post events
          │ (transport-free)  │   ← written ONCE (clients/react/src/render + area)
          └─────────┬─────────┘
       DOM leaf pack        RN leaf pack
   web / Electron        iOS / Android

This is the same shape the platform uses everywhere: MAUI renders the identical tree with a native MauiViewPack, Blazor with its web views — the React renderer adds a Fluent UI React v9 pack (near 1:1 with the Blazor portal's Fluent UI Blazor components).

The wire model: UiControl trees

A layout area is delivered as a single JSON object with an areas map (area key → control) and a data map (values that bindings point at). From clients/react/src/area/types.ts:

export interface UiControl {
  $type: string;              // short name: "Stack", "Label", "DataGrid", ...
  id?: Json;
  dataContext?: string;       // pointer prefix for relative bindings
  style?: Json;
  class?: Json;
  skins?: Skin[];             // container/wrapper skins, popped LIFO
  isClickable?: boolean;
  pageTitle?: Json;
  [key: string]: Json;
}

export interface AreaTree {
  areas?: Record<string, UiControl>;
  data?: Record<string, Json>;
}

Containers don't inline their children — they carry an areas list of NamedArea references, each pointing at another key in the areas map. A control property is either a literal value or a binding: a JSON pointer into /data, resolved (and written back) live. This is exactly the contract described in Data Binding; the React renderer is another consumer of it.

AreaSource — the one seam

The renderer depends on nothing but this interface:

export interface AreaSource {
  getState(): AreaTree;                          // current {areas, data} snapshot
  subscribe(listener: () => void): () => void;   // change notification
  emit(event: MeshEvent): void;                  // clicks, edits, blur, closeDialog
}

Two implementations ship:

MeshAreaView — the composed entry point

MeshAreaView (the package's top-level component, clients/react/src/index.tsx) stacks the four providers the renderer needs and renders the root area:

export function MeshAreaView({ source, rootArea, theme, themeStorageKey, ops }: MeshAreaViewProps) {
  const { theme: preferredTheme } = useThemeMode({ storageKey: themeStorageKey });
  return (
    <FluentProvider theme={theme ?? preferredTheme}>
      <MeshOpsProvider ops={ops ?? null}>
        <RegistryProvider pack={fluentPack}>
          <ScopeProvider source={source} area={rootArea}>
            <RenderArea areaKey={rootArea} />
          </ScopeProvider>
        </RegistryProvider>
      </MeshOpsProvider>
    </FluentProvider>
  );
}

The registry: $type → component

Dispatch is data-driven. The active pack is a plain record of maps (render/registryContext.tsx):

export type ControlComponent = (props: { control: UiControl }) => ReactNode;
export type SkinComponent = (props: { skin: Skin; control: UiControl }) => ReactNode;

export interface LeafPack {
  controls: Record<string, ControlComponent>;  // $type → component (leaf controls)
  skins: Record<string, SkinComponent>;        // skin $type → wrapper (Stack/Tabs/Card/…)
  fallback: ControlComponent;                  // renders an unknown $type
  defaultContainer: SkinComponent;             // container with no remaining skin
}

The shipped web pack is fluentPack (render/registry.tsx): controlRegistry spreads the per-category maps (display, inputs, data, nav, feedback, containers, editors, mesh, appearance, item templates) and skinRegistry covers the container skins. Spreading your own entries over controlRegistry is the designed extension point — see Custom React Controls.

ControlRenderer (render/ControlRenderer.tsx) implements the dispatch:

  1. Skins pop LIFO — the last skin wraps (or, for layout skins, lays out) the control, recursing with the rest.
  2. A container with no remaining skin renders through defaultContainer (a flex stack).
  3. A leaf dispatches $type against pack.controls; unknown types render pack.fallback — a clearly-labeled "Unsupported control", never a crash.

The lookup tolerates both $type spellings: registries hold the suffix-stripped short names ("Stack", "LayoutStack"), while the live mesh serializes class names ("StackControl", "LayoutStackSkin") — the exact key is tried first, then the suffix-stripped one.

Child areas resolve through the same primitives your own controls can use:

export { ControlRenderer, RenderArea, RenderChildren, useChildAreas } from "./render/ControlRenderer.js";
export { ScopeProvider, useAreaState, useResolve, useEmit, useScope } from "./area/context.js";

useResolve(control.someProp) returns the property's value whether it is a literal or a bound /data pointer; useEmit() posts events into the source; useAreaState() is the live tree (backed by useSyncExternalStore, so React re-renders exactly the consumers of changed state).

Live hydration: GrpcAreaSource over Connect + Deliver

Browsers can't open the mesh's bidirectional gRPC Open stream (no HTTP/2 duplex from fetch), so the server splits the duplex into a server-streaming Connect (mesh → client) and a unary Deliver (client → mesh). MeshWebConnection in @meshweaver/client-web (clients/grpc-web/src/connection.ts) hides the split behind the same surface the Node SDK exposes — observe / post / watch:

GrpcAreaSource builds the area subscription on watch:

import { connect } from "@meshweaver/client-web";
import { GrpcAreaSource, MeshAreaView } from "@meshweaver/react";

const conn = await connect("https://memex.meshweaver.cloud", { token: "mw_..." });
const source = new GrpcAreaSource(conn, "ACME/MyApp", { area: "Overview" });
void source.start();   // folds the live area stream into {areas, data}
// <MeshAreaView source={source} rootArea="Overview" />

The wire behavior it pins (documented in live/grpcSource.ts against the server sources):

One registry per connection. A single MeshAreaRegistry (live/grpcSource.ts) owns every GrpcAreaSource over a connection: the routed page area and every nested @@ / LayoutAreaControl embed resolve through it, so a given (address, area, id) has exactly one live watch stream shared by all consumers (createGrpcEmbeddedFactory is its embed-factory view). Creating a fresh source per render or per embed re-subscribes on every re-render — the same non-determinism the version fold guards against, one level up. See the overview's Live connection & session for the session-level picture.

That is why a transport blip doesn't kill the UI the way a dropped Blazor circuit does: the browser holds the whole {areas, data} state, and the connection's auto-reconnect re-opens the subscription and replays it, yielding a fresh Full snapshot to converge on — nothing server-side needs the old connection's memory.

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.