github tokio-rs/topcoat v0.9.0

5 hours ago
  • Server push with live! and connected
  • Suspense and error boundaries
  • Stable identities for state and live regions
  • More capable runtime expressions
  • Stateful hot reload
  • Reactive UI components, sidebars, and form fields
  • Routing and server integration
  • Client IPs and trusted proxies
  • Cheaply cloneable errors
  • Upgrading from 0.8.1

Server push with live! and connected

Keep server-rendered content updating after the page finishes loading. live! and emit! render the updates; connected(cx) asks Topcoat to keep them flowing over a WebSocket.

Here's the chat example. Its Chat app state stores messages and notifies subscribers when a message arrives:

use topcoat::{
    Result,
    context::{Cx, app_context},
    runtime::{connected, shard},
    view::{View, emit, live},
};

#[shard]
async fn chat_box(cx: &Cx) -> Result<impl View> {
    Ok(live! {
        let chat = app_context::<Chat>(cx);
        let mut changed = chat.subscribe();
        loop {
            let token = emit! {
                <ul>
                    for message in chat.messages() {
                        <li>(message)</li>
                    }
                </ul>
            }?;
            if !connected(cx) {
                break Ok(token);
            }
            changed.recv().await.ok();
        }
    })
}

The HTTP render shows the current messages and finishes. The browser then connects and runs the shard again with connected(cx) == true. Send a message in one tab and every connected tab updates.

Connections can belong to a whole page or one shard, with automatic reconnection and reuse of enclosing connections. Shards also support streaming over ordinary HTTP, so loading states and results can arrive progressively without opening a socket.

Upgrade: Enable .runtime(), serve the asset bundle, and include topcoat::runtime::script() in your document. See the live guide for setup and lifecycle details.

Suspense and error boundaries

Suspense skips the fallback when content is ready immediately. The new SuspenseMode::Wait waits for initial content instead of streaming a fallback, making that content available without JavaScript:

suspense(
    fallback: view! { <p>"Loading..."</p> },
    mode: SuspenseMode::Wait,
    product()
)

Set it per boundary, through context, or for the router with .suspense(SuspenseMode::Wait). Error boundaries can replace failed content even after it starts streaming. See the suspense example.

Stable identities for state and live regions

Live region IDs stay stable across rerenders. The new #[key(...)] loop attribute gives each item's components and live regions a stable identity:

view! {
    #[key(post.id)]
    for post in posts {
        post_card(title: post.title)
    }
}

Upgrade: replace component key: arguments with keyed loops. For repeated Rust helper calls, pass &cx.keyed(item.id). See the identity guide.

More capable runtime expressions

Runtime expressions now support all Rust integer types with full precision, including 128-bit integers, plus vectors, arrays, and slices. Expressions also compose without losing reactivity:

let count = signal(cx, || 0usize);
let next = expr!(count.get() + 1);
let label = expr!(if next > 10 { "Plenty" } else { "Keep going" });

Expressions without signal reads render as static content. Shard arguments accept plain values or expressions. The browser runtime now uses Topcoat's own reactive engine, with Rust/JavaScript coherence tests checking matching behavior.

Stateful hot reload

topcoat dev updates the existing page while preserving matching form controls and signal values. Keep a dialog open or a form filled in while editing its markup.

Signal state survives while identities match. Script, base URL, or doctype changes trigger a full reload; a manual reload resets state.

Reactive UI components, sidebars, and form fields

Dialogs, sheets, and tab state can now take runtime expressions:

dialog(
    open: $(is_open.get()),
    dialog_content("Hello!")
)

The registry adds responsive sidebars with separate desktop and mobile state, and form field components for labels, descriptions, errors, and responsive layouts:

topcoat ui add sidebar
topcoat ui add field

The neutral theme adds card, popover, and sidebar colors, alongside typography and spacing refinements. Existing apps can update component source with topcoat ui update and merge the new theme tokens. Explore the UI example.

image

Routing and server integration

Procedures and shards can have explicit endpoint paths:

#[procedure("/api/double")]
async fn double(value: usize) -> Result<usize> {
    Ok(value * 2)
}

The same syntax works for #[shard("/search/results")]. Upgrade: register both with .route(name); .discover() still works.

Serve files at fixed URLs with the fs feature and RouterBuilderDirectoryExt:

let router = Router::builder()
    .public_dir("./public")
    .serve_dir("/downloads/{*file}", "./files")
    .build();

Other routing changes:

  • Page reruns use the page's own URL and restore signal values through RuntimeLayer. Register application layers before .runtime().
  • StripPrefixLayer gives mounted services paths relative to their mount point.
  • Rewrites use fresh request contexts and support header replacement and POST to GET at the same URL. Upgrade: use .with(value) instead of .cx(cx.with(value)) to carry state. Router::handle_with also accepts explicit context values.

See the router guide and rewrite guide.

Client IPs and trusted proxies

client_ip(cx) resolves the client's IP address through trusted reverse proxies. Configure which proxies can supply forwarding headers with TrustedProxies:

use topcoat::router::{Router, TrustedProxies};

let router = Router::builder()
    .trusted_proxies(TrustedProxies::new().networks(["10.0.0.0/8"]))
    .build();

Read the address with topcoat::router::request::client_ip(cx). No proxies are trusted by default; remote_addr(cx) always exposes the direct connection. You can also trust a fixed number of hops with .nearest(n) when every request passes through those proxies. See the proxy API.

Cheaply cloneable errors

topcoat::Error is now cheap to clone, making it easier to share errors from memoized work. Context, backtraces, and downcasting remain available, and shared router errors retain their HTTP status.

Upgrade: Error no longer wraps anyhow::Error. Use Error::from_anyhow with the anyhow feature for conversion. Owned and mutable downcasts require unique ownership; use downcast_cloned when sharing is expected. See the error API.

Upgrading from 0.8.1

Update your topcoat dependency to 0.9 and install the matching CLI:

cargo install topcoat-cli --version 0.9.0 --locked

Enable the runtime explicitly on your router. The Cargo feature and browser script alone are not enough for page reruns and WebSocket connections. Import RouterBuilderRuntimeExt and call .runtime() once, after registering application layers:

use topcoat::{
    asset::{AssetBundle, RouterBuilderAssetExt},
    router::{Router, RouterBuilderDiscoverExt},
    runtime::RouterBuilderRuntimeExt,
};

let router = Router::builder()
    .discover()
    .assets(AssetBundle::load().unwrap())
    // Register application layers before .runtime().
    .runtime()
    .build();

Keep topcoat::runtime::script() in your document's <head>. Putting .runtime() after your layers lets it convert page rerun requests to GET before those layers run.

For the APIs you use:

  • Procedures and shards: replace .procedure(name) and .shard(name) with .route(name), or keep using .discover(). Remove the old registration trait imports.
  • Keys: replace component key: props with #[key(...)] on loops. Use cx.keyed(...) for repeated Rust helper calls.
  • Errors: convert anyhow errors with Error::from_anyhow and enable the anyhow feature. Update downcast error handling, or use downcast_cloned for shared errors.
  • Rewrites: replace .cx(cx.with(value)) with .with(value).
  • Vendored UI: run topcoat ui update and merge new theme tokens into your stylesheet.

To add server push, emit current content first, return when connected(cx) is false, and wait for updates only in the connected render.

Don't miss a new topcoat release

NewReleases is sending notifications on new releases.