# How to Use Datastar for Server-Sent Events and Reactive Updates in Topcoat

> Learn to implement server-sent events and reactive updates in Topcoat with Datastar. Stream SSE to patch the DOM and merge signals for dynamic web applications. Enable the datastar Cargo feature for extractors and response types.

- Repository: [Tokio/topcoat](https://github.com/tokio-rs/topcoat)
- Tags: how-to-guide
- Published: 2026-07-31

---

**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`](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat/src/datastar.rs). Enabling this feature also activates the router's underlying `sse` infrastructure, allowing your application to stream events.

```toml
[dependencies]
topcoat = { version = "0.1", features = ["datastar"] }

```

### Loading the Client-Side Script

The browser must load the [`datastar.js`](https://github.com/tokio-rs/topcoat/blob/main/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.

```rust
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`](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat/src/datastar.rs) handles this deserialization automatically.

```rust
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.

```rust
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`](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat-datastar/src/patch_signals.rs), supports flags like `only_if_missing` to prevent overwriting existing values.

```rust
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.

```rust
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.

```rust
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.

```rust
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 `datastar` feature** in your [`Cargo.toml`](https://github.com/tokio-rs/topcoat/blob/main/Cargo.toml) to unlock Datastar extractors and SSE support in [`crates/topcoat/src/datastar.rs`](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat/src/datastar.rs).
- **Load the Datastar script** via CDN or the `asset!` macro before using `data-*` attributes.
- **Use `Signals<T>`** to deserialize client state from query strings or JSON bodies.
- **Return `PatchElements`** to morph specific DOM nodes using selectors and modes like `Prepend` or `Inner`.
- **Return `PatchSignals`** to merge JSON into the client signal store, with optional flags like `only_if_missing` as implemented in [`crates/topcoat-datastar/src/patch_signals.rs`](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat-datastar/src/patch_signals.rs).
- **Stream `Sse` responses** for live updates, converting patches into `Event` types automatically.
- **Execute JavaScript** with `ExecuteScript` for 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`](https://github.com/tokio-rs/topcoat/blob/main/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`](https://github.com/tokio-rs/topcoat/blob/main/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.