MeshWeaver draws a clean line between static views (rendered once and forgotten) and dynamic views (re-rendered every time an observable emits). Getting this distinction right is the foundation of an efficient, responsive UI. Static views render once with no subscription overhead; dynamic views subscribe to an observable and surgically re-render only the affected area on each emission.
Static Views
A static view is computed exactly once — when the layout area first loads. No subscription is created and no watcher is registered. This makes it the most efficient option for content that never needs to change.
Controls.Stack
.WithView(Controls.Html("<h2>Dashboard</h2>"))
.WithView(Controls.Label("This text never changes"))
Use static views for: headers, navigation, action buttons with fixed labels, and any content that is determined entirely at render time.
Dynamic Views
Wrap a control in an IObservable<T> and MeshWeaver subscribes to it. Every time the observable emits, only that area of the DOM is replaced — the rest of the page is untouched.
var counter = Observable.Interval(TimeSpan.FromSeconds(1));
Controls.Stack
.WithView(Controls.Label("Page Title")) // static — never changes
.WithView(counter.Select(n => Controls.Label($"Count: {n}"))) // dynamic — updates each second
The subscription is created when the area is mounted and disposed automatically when it is removed. Only the specific area backed by the observable participates in the update cycle.
Real-Time Clock Example
A live clock shows the pattern at its simplest — a single observable drives continuous DOM updates with no explicit state management:
var tick = Observable.Interval(TimeSpan.FromSeconds(1));
Controls.Stack
.WithView(tick.Select(_ => Controls.Label(DateTime.Now.ToString("HH:mm:ss"))))
Combining Static and Dynamic
In practice, most layout areas are a mix: structural scaffolding is static, and only the data-driven regions are reactive. This keeps subscriptions narrow and re-renders cheap.
Controls.Stack
.WithView(Controls.Html("<h1>Dashboard</h1>")) // static header
.WithView(metricsStream.Select(m => BuildMetrics(m))) // dynamic content
.WithView(Controls.Button("Refresh")) // static button
The header and button are never touched by the update cycle. Only the middle area re-renders when metricsStream emits.
Loading Data First
When you need data before producing a control, return an observable that emits once the data arrives. A view delegate runs on the layout hub's render path, so this is the shape to reach for first:
// ✅ Compose — the area renders as soon as the stream emits
Controls.Stack
.WithView((host, ctx) =>
host.Workspace.GetMeshNodeStream(userPath)
.Where(node => node is not null)
.Select(node => Controls.Label($"Hello, {node.Name}")))
ViewDefinition is declared as Task<UiControl?>, so WithView also accepts an async delegate that runs once at render time — it is not a subscription, it fires exactly once:
Controls.Stack
.WithView(async (host, ctx, ct) => {
// 🚨 ViewDefinition's Task<UiControl?> is a signature we do not own, so an
// await is legitimate here — the BRIDGE is not free choice. ObserveCompletion,
// never Rx's ToTask bridge (forbidden repo-wide, 2026-08-30) and never a bare
// `await someObservable`: both resume this render INLINE on the pool thread
// that signalled, still inside Rx's trampoline.
var settings = await ioPool.Invoke(ct2 => LoadSettingsAsync(ct2))
.FirstAsync()
.ObserveCompletion(
ex => logger.LogWarning(ex, "settings load faulted after the wait settled"),
ct);
return Controls.Label($"Hello, {settings.Name}");
})
🚨 The signature allows
await; the hub does not forgive it. Never await hub-reachable work here — a mesh read, aQueryAsync, a permission lookup, another layout area. That is a render running on the hub scheduler waiting for the hub, which is how layout areas deadlock or freeze at "awaiting first data". External I/O must go through anIIoPool, never a bareawaitand neverObservable.FromAsync(which runs the call's synchronous prologue on the subscribing thread and bounds nothing).For anything that comes from the mesh — which is almost everything — do not fetch at all: pass the path and let the view bind, or use the observable overload above. See Data Binding.
Reacting to Data Changes
When the content depends on a data stream, expose that stream directly via WithView. The area subscribes and stays in sync for as long as it is mounted:
Controls.Stack
.WithView((host, ctx, store) =>
host.GetDataStream<User>("currentUser")
.Select(user => Controls.Label($"Logged in as {user.Name}")))
How Re-rendering Works
Understanding the internals helps avoid surprises. When a dynamic view receives a new value, the framework:
- Subscribes to the observable with
DistinctUntilChanged()to skip identical emissions. - Calls
UpdateAreaon each new value. - Removes the existing controls in that area.
- Renders the new control in their place.
- Writes only the affected DOM elements to the client.
// Framework internals — shown for clarity, not for direct use
generator
.DistinctUntilChanged()
.Subscribe(view => UpdateArea(context, view))
The key insight is that only the area backed by the emitting observable updates. Everything else on the page remains untouched.
Common Patterns
Conditional Content
Toggle between controls based on a boolean stream:
isLoadingStream.Select(loading =>
loading
? Controls.ProgressRing()
: Controls.Label("Ready"))
Computed Display
Derive a rendered value from a data stream in a single expression:
dataStream.Select(data =>
Controls.Markdown($"**Total:** {data.Items.Sum(i => i.Value)}"))
Combined Streams
Combine two independent streams into a single view — the area updates whenever either stream emits:
userStream.CombineLatest(settingsStream, (user, settings) =>
Controls.Label($"{user.Name} - {settings.Theme}"))
Debounced Updates
Throttle high-frequency input before building controls:
searchStream
.Debounce(TimeSpan.FromMilliseconds(300))
.Select(term => BuildSearchResults(term))
Observable Operators for UI
These Rx operators appear most often in MeshWeaver UI code:
| Operator | Purpose | Typical use |
|---|---|---|
Select |
Transform data to a control | stream.Select(d => Controls.Label(d.Name)) |
DistinctUntilChanged |
Skip duplicate emissions | stream.DistinctUntilChanged() |
Debounce |
Reduce update frequency | stream.Debounce(TimeSpan.FromMilliseconds(100)) |
CombineLatest |
Merge two streams into one view | a.CombineLatest(b, (x, y) => ...) |
StartWith |
Provide an initial value before the first emission | stream.StartWith(defaultValue) |
Performance Guidelines
The most important rule: keep dynamic regions small. A large re-render is always more expensive than a small one, even if the data-fetch is fast.
| Scenario | Recommendation |
|---|---|
| Headers, labels, navigation, buttons with fixed text | Static view — no subscription overhead |
| Data displays, computed values, user-input-dependent content | Dynamic view — scoped to only the changing area |
| High-frequency streams (e.g. search input) | Add Debounce() to reduce render churn |
| Streams that frequently re-emit the same value | Add DistinctUntilChanged() to skip no-op renders |
Click Actions Are Reactive, Not Async
🚨 ABSOLUTE: Never use
awaitinside aWithClickActionhandler. The handler runs inside the layout hub's message pump. Awaiting a mesh-backed service inside the pump deadlocks it. ComposeIObservable<T>chains instead and callSubscribe.
// ✅ Correct — synchronous handler, observable chain for the async work
.WithClickAction(ctx =>
{
ctx.Host.UpdateData(statusId, "<p>Working…</p>"); // immediate feedback
ctx.Host.Stream.GetDataStream<Dictionary<string, object?>>(formId)
.Take(1)
.Subscribe(data =>
{
var input = data?.GetValueOrDefault("field")?.ToString() ?? "";
myService.DoReactive(input).Subscribe(
result => ctx.Host.UpdateData(statusId, $"<p>Done: {result}</p>"),
ex => ctx.Host.UpdateData(statusId, $"<p>Error: {ex.Message}</p>"));
});
return Task.CompletedTask; // the handler itself is synchronous
})
// Note: the `.Take(1)` above is a one-shot FORM READ inside a click action —
// fine, BECAUSE the form id has definitely been written by the time the user
// can click it. 🚨 On an id that was NEVER written, `GetDataStream(id)` emits
// NOTHING (not null, not a default) and never completes, so `.Take(1)` never
// fires: as a "run this once" guard it does not block a duplicate run, it
// blocks the FIRST one — silently, with no exception to grep. Need a value for
// an id that may be unset? `.StartWith(default)` or seed it with
// `host.UpdateData(id, …)` first. Pinned by `GetDataStreamUnsetIdTest`.
// And NEVER `.Take(1)` a stream that feeds a live-bound view: the binding
// freezes on the first emission (see Data Binding).
// ❌ Wrong — async handler deadlocks the pump under load
.WithClickAction(async ctx =>
{
var data = await ctx.Host.Stream.GetDataStream<T>(id).FirstAsync();
var result = await myService.DoWorkAsync(data);
ctx.Host.UpdateData(statusId, result);
})
See AsynchronousCalls for the full rationale and additional patterns.
See Also
- Container Control — Adding content to containers
- Data Binding — How data flows to controls
- Editor Control — Real-world dynamic examples
- Asynchronous Calls — Why
awaitdeadlocks and how to compose observables