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 connection to the server was interrupted. Trying to restore it…
Trying again…
The connection could not be restored. Reloading the page…
The server was updated. Reloading the page to pick up the latest version.