- Server push with
live!andconnected - 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 fieldThe 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.
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(). StripPrefixLayergives mounted services paths relative to their mount point.- Rewrites use fresh request contexts and support header replacement and
POSTtoGETat the same URL. Upgrade: use.with(value)instead of.cx(cx.with(value))to carry state.Router::handle_withalso 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 --lockedEnable 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. Usecx.keyed(...)for repeated Rust helper calls. - Errors: convert anyhow errors with
Error::from_anyhowand enable theanyhowfeature. Update downcast error handling, or usedowncast_clonedfor shared errors. - Rewrites: replace
.cx(cx.with(value))with.with(value). - Vendored UI: run
topcoat ui updateand 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.