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 server was updated. Reloading the page to pick up the latest version.