React Frontend

MeshWeaver has two web frontends over one UI contract. The classic portal is Blazor Server: layout areas render server-side and DOM diffs stream to the browser over a SignalR circuit. The React frontend renders the same layout areas client-side: the browser receives the raw UiControl JSON tree (an {areas, data} snapshot plus patches) over the gRPC-web mesh transport and a React + Fluent UI renderer walks it. Same hubs, same layout areas, same data binding — a different last mile.

The code on these pages does not execute in the portal. The live --render blocks used elsewhere in the docs run C# in the portal's kernel; React/TSX cannot execute there. All TSX snippets in this section are plain fenced code, verified against the sources in clients/react, clients/grpc-web, and clients/portal-next.

The three packages

Package Location Role
@meshweaver/react clients/react The renderer: $type → Fluent UI React v9 component registry, skin dispatch, binding hooks, theming, ThreadChat. @meshweaver/react/core is the Fluent-free core for React Native / custom leaf packs.
@meshweaver/client-web clients/grpc-web The browser/React-Native mesh transport over gRPC-web (Connect-ES) — observe / post / watch plus the Mesh operations surface (search, patch, startThread, submitMessage, …).
portal app shell clients/portal-next (deployed) · clients/portal (example) clients/portal-next is the Next.js streaming-SSR shell — header + nav chrome around the renderer, holding the one live connection and area registry described under Live connection & session below; the web analog of the Blazor portal shell. clients/portal is a standalone client-only Vite example of the same idea, served at /app.

Architecture vs. Blazor Server

Blazor Server (classic) React frontend
Rendering Server-side; DOM diffs over the SignalR circuit Client-side; the browser receives the UiControl JSON tree and renders it locally
Transport SignalR circuit (stateful, per-tab) gRPC-web Connect (server-stream) + Deliver (unary) — see Rendering Architecture
Connection loss The circuit is the UI state — a drop degrades to the reconnect overlay / a page reload No server circuit. The UI state lives in the browser; the layout-area stream is a Full snapshot + patches, so re-opening the subscription re-syncs the tree instead of killing the UI
Interactivity Events round-trip to the server component Events post back as mesh messages (ClickedEvent, PatchDataChangeRequest, …); edits apply optimistically and the server echoes the authoritative patch
Extensibility Blazor views registered per control type A spreadable $type → component registry; new controls load at runtime via native ESM import(url) — see Custom React Controls

Both frontends bind data the same way conceptually: the backend layout area declares what to render, and every value read/write rides the area's data stream (see Data Binding for the contract). The React renderer resolves the same /data JSON-pointer bindings with its useResolve hook.

Live connection & session

The browser can't hold the bidirectional Open stream a native participant uses (no HTTP/2 duplex), so @meshweaver/client-web splits it into a server-streaming Connect (mesh → browser) plus a unary Deliver (browser → mesh); the Connect ack carries the connectionId every Deliver quotes back. One connection per browser tab multiplexes every area/node subscription over that single Connect stream, keyed by streamId.

Four rules make that connection behave like a real, stable mesh participant — get any of them wrong and the page renders a random subset of its regions, differently on each reload:

Because the SSR layer holds no stream, the first paint is a server-rendered snapshot; the live subscription takes over deterministically once its first Full frame folds. See Rendering Architecture for the snapshot → live handoff.

Ingress note: the Connect server-stream must not be buffered by a reverse proxy — nginx-ingress needs nginx.ingress.kubernetes.io/proxy-buffering: "off" on the web ingress, or a streaming response is held and truncated. See the deployment chart's ingress.annotations.

Parity state

Parity with the Blazor portal is pinned by a test, not by intention. clients/react/src/render/parity.test.ts lists the authoritative Blazor vocabulary — every *Control / *Skin type in src/MeshWeaver.Layout — and fails when the React pack misses one:

Everything — containers, forms, grids, charts, markdown, nav, dialogs, editors, the chat, document export/import, the file browser — renders for real. See Testing & Parity for the full test story.

Topic map

Page What it covers
Getting Started The served SPA at /app, the Portal:Frontend / Portal:ReactAppUrl configuration, the /frontend/{react\|blazor\|clear} toggle + mw-frontend cookie, and local dev with Vite
Rendering Architecture The UiControl JSON contract, the $type → component registry, MeshAreaView + AreaSource, and how live areas hydrate over gRPC-web
Theming Light/dark/system with the same localStorage contract as Blazor — one preference across both frontends
Thread Chat The React chat: thread-node watching, message satellites, composer gating, startThread / submitMessage
Custom React Controls Extending the renderer with your own control — server-side UiControl subclass + a React component for its $type, runtime ESM loading
Custom Blazor Controls The Blazor half of the same job — a BlazorView for the control type
Testing & Parity The parity ratchet, the vitest suites, and the transport round-trip test
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.