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 VIEW DYNAMIC VIEW Layout Area Loads Controls computed once at render time DOM Written Once No subscription, no watcher registered Idle — never re-renders Headers, labels, fixed buttons Layout Area Mounts Observable subscribed automatically Observable Emits DistinctUntilChanged — skip no-ops Only That Area Re-renders Rest of the page untouched next emit 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, a QueryAsync, 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 an IIoPool, never a bare await and never Observable.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:

  1. Subscribes to the observable with DistinctUntilChanged() to skip identical emissions.
  2. Calls UpdateArea on each new value.
  3. Removes the existing controls in that area.
  4. Renders the new control in their place.
  5. 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 await inside a WithClickAction handler. The handler runs inside the layout hub's message pump. Awaiting a mesh-backed service inside the pump deadlocks it. Compose IObservable<T> chains instead and call Subscribe.

// ✅ 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

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