How to Use Datastar for Server-Sent Events and Reactive Updates in Topcoat
Datastar enables real-time reactive updates in Topcoat by streaming Server-Sent Events (SSE) that patch the DOM and merge signals, using the datastar Cargo feature to enable extractors like Signals<T> and response types like PatchElements and Sse.
Datastar is a lightweight client-side framework that integrates with Topcoat to push incremental UI changes from the server to the browser. When you enable the datastar feature in your Topcoat application, you unlock specialized extractors and response types that handle reactive state management and server-sent events without writing JavaScript boilerplate.
Setting Up Datastar in Topcoat
Before using reactive features, you must enable the Datastar integration and ensure the browser loads the client-side library.
Enabling the Cargo Feature
Datastar support is gated behind the datastar Cargo feature in crates/topcoat/src/datastar.rs. Enabling this feature also activates the router's underlying sse infrastructure, allowing your application to stream events.
[dependencies]
topcoat = { version = "0.1", features = ["datastar"] }
Loading the Client-Side Script
The browser must load the datastar.js module before any data-* attributes become active. In Topcoat, you can serve this via CDN or as a static asset using the asset! macro.
use topcoat::{asset::asset, router::layout, view::view, Result};
#[layout]
async fn root(slot: Result) -> Result {
view! {
<!DOCTYPE html>
<html>
<head>
<script
type="module"
src=(asset!("https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.2/bundles/datastar.js"))
></script>
</head>
<body>(slot?)</body>
</html>
}
}
Handling Reactive Signals with Extractors
Datastar maintains a client-side signal store that syncs with the server on every request. The Signals extractor deserializes this state into a Rust type, allowing handlers to read and modify reactive data.
Every Datastar request includes the current signal store either in the datastar query string for GET requests or as a JSON body for other methods. The Signals<T> type in crates/topcoat/src/datastar.rs handles this deserialization automatically.
use serde::{Deserialize, Serialize};
use topcoat::{datastar::{PatchSignals, Signals}, router::route, Result};
#[derive(Deserialize, Serialize)]
struct Counter { count: u64 }
#[route(POST "/increment")]
async fn increment(Signals(counter): Signals<Counter>) -> Result<PatchSignals> {
PatchSignals::json(&Counter { count: counter.count + 1 })
}
Patching the DOM and Signals
Topcoat provides two primary mechanisms for updating the client: PatchElements for DOM manipulation and PatchSignals for state merging.
Updating HTML with PatchElements
PatchElements contains HTML fragments that replace or modify existing DOM nodes. By default, Datastar uses the fragment's id attribute to locate the target element, but you can override this with the selector and mode methods.
Available patch modes include Inner, Append, Prepend, and Outer, defined in the Datastar implementation.
use topcoat::{datastar::{ElementPatchMode, PatchElements}, context::Cx, router::route, view::view, Result};
#[route(POST "/entries")]
async fn create(cx: &Cx) -> Result<PatchElements> {
let entry = view! { <li>"A new entry"</li> }?;
Ok(PatchElements::new(entry.render(cx))
.selector("#feed")
.mode(ElementPatchMode::Prepend))
}
Merging State with PatchSignals
PatchSignals encodes a JSON object that merges into the browser's signal store. This type, implemented in crates/topcoat-datastar/src/patch_signals.rs, supports flags like only_if_missing to prevent overwriting existing values.
use serde::Serialize;
use topcoat::datastar::PatchSignals;
#[derive(Serialize)]
struct UserStatus { online: bool }
let patch = PatchSignals::json(&UserStatus { online: true });
Streaming Live Updates with Server-Sent Events
For continuous, real-time updates, handlers return an Sse stream where each item implements Into<Event>. Both PatchElements and PatchSignals convert into Event types automatically, allowing you to stream DOM patches or signal updates as server-sent events.
The Sse type integrates with Topcoat's existing SSE infrastructure, which is enabled automatically when you activate the datastar feature.
use futures_core::Stream;
use futures_util::stream;
use serde::Serialize;
use topcoat::{
datastar::PatchSignals,
router::{
content::sse::{Event, KeepAlive, Sse},
route,
},
Result,
};
#[derive(Serialize)]
struct Progress { percent: u8 }
#[route(GET "/progress")]
async fn progress() -> Result<Sse<impl Stream<Item = Result<Event>> + use<>>> {
let events = stream::iter((0..=100u8).step_by(20).map(|percent| {
PatchSignals::json(&Progress { percent }).map(Into::into)
}));
Ok(Sse::new(events).keep_alive(KeepAlive::new()))
}
Advanced Response Patterns
Beyond streaming patches, Topcoat supports executing arbitrary JavaScript and returning plain HTML with Datastar metadata.
Executing Arbitrary Scripts
ExecuteScript builds a temporary <script> element that runs JavaScript in the browser. This is useful for side effects that cannot be expressed through DOM or signal patches, such as triggering browser APIs or third-party library calls.
use topcoat::datastar::ExecuteScript;
let script = ExecuteScript::new("console.log('saved')");
Plain HTML Responses with Metadata
Handlers can return standard View responses alongside DatastarSelector and DatastarMode values. Response headers communicate the target element and patch mode to the Datastar client, allowing fine-grained control over how HTML fragments are applied.
use topcoat::{
datastar::{DatastarMode, DatastarSelector},
router::route,
view::{view, View},
Result,
};
#[route(POST "/save")]
async fn save() -> Result<(DatastarSelector, DatastarMode, View)> {
let status = view! { <p>"Saved!"</p> }?;
Ok((
DatastarSelector::from("#status"),
DatastarMode(ElementPatchMode::Inner),
status,
))
}
Summary
- Enable the
datastarfeature in yourCargo.tomlto unlock Datastar extractors and SSE support incrates/topcoat/src/datastar.rs. - Load the Datastar script via CDN or the
asset!macro before usingdata-*attributes. - Use
Signals<T>to deserialize client state from query strings or JSON bodies. - Return
PatchElementsto morph specific DOM nodes using selectors and modes likePrependorInner. - Return
PatchSignalsto merge JSON into the client signal store, with optional flags likeonly_if_missingas implemented incrates/topcoat-datastar/src/patch_signals.rs. - Stream
Sseresponses for live updates, converting patches intoEventtypes automatically. - Execute JavaScript with
ExecuteScriptfor side effects outside the reactive data flow.
Frequently Asked Questions
How does Datastar differ from traditional HTMX in Topcoat?
Datastar uses a reactive signal-based architecture where the server can push state updates via Server-Sent Events, whereas HTMX typically focuses on request-response HTML swapping. In Topcoat, Datastar leverages the same underlying SSE infrastructure but adds automatic signal synchronization and the Signals<T> extractor for type-safe state management.
Can I use Datastar with existing Topcoat routes that return regular HTML?
Yes. Datastar integrates incrementally. Routes can return standard View types, and you can opt-in to Datastar behavior by returning tuples like (DatastarSelector, DatastarMode, View) or by using the datastar feature only on specific routes. The client-side library activates only when the datastar.js script is present in the layout.
What patch modes are available when updating the DOM?
The ElementPatchMode enum supports Inner (default, replaces children), Append, Prepend, and Outer (replaces the element itself). These modes control how the HTML fragment from PatchElements is inserted relative to the target selector, giving you granular control over DOM mutations without writing client-side JavaScript.
How do I prevent Datastar from overwriting existing client signals?
Use the only_if_missing flag when constructing PatchSignals. According to the implementation in crates/topcoat-datastar/src/patch_signals.rs, this option ensures the JSON payload merges into the signal store only if the keys do not already exist, protecting client-side state from being clobbered by server updates.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →