# How Topcoat Implements Client-Side Reactivity Without JavaScript Bundles

> Discover how Topcoat enables client-side reactivity without JavaScript bundles by compiling Rust to JS, delivering a single runtime script for efficient DOM updates and signal management.

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

---

**Topcoat achieves zero-bundle client-side reactivity by compiling Rust expressions into JavaScript at compile time and injecting a single, self-contained runtime script that manages reactive signals and DOM updates directly in the browser.**

The tokio-rs/topcoat framework eliminates traditional JavaScript build pipelines by embedding a minimal runtime and generating executable client code directly from Rust macros. This architecture allows developers to write interactive server-rendered applications without WebAssembly or separate frontend bundling steps.

## The Single-Script Runtime Architecture

Topcoat’s reactivity depends on a tiny browser script shipped as a static asset. The function `topcoat::runtime::script()` inserts a `<script type="module">` tag that loads [`browser/dist/index.js`](https://github.com/tokio-rs/topcoat/blob/main/browser/dist/index.js) from the runtime crate. This file, declared in [`crates/topcoat-runtime/src/lib.rs`](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat-runtime/src/lib.rs) as the `SCRIPT` asset, constitutes the only JavaScript delivered to the client.

To enable the runtime, register the asset bundle with your router using `AssetBundle::load()`:

```rust
use topcoat::runtime::AssetBundle;

let app = Router::new()
    .merge(AssetBundle::load());

```

The script is served directly without modification, tree-shaking, or additional compilation, ensuring immediate execution upon page load.

## Compile-Time JavaScript Generation

When the `view!` macro encounters a `$(…)` block, the `expr!` procedural macro parses the Rust expression and generates dual representations. Located in `crates/topcoat-runtime/grammar/src/expr/`, this parser creates:

1. A Rust expression executed during server-side rendering
2. A JavaScript snippet embedded directly into the HTML response

For example, writing `$(count.get())` inside a template causes the macro to emit equivalent JavaScript that reads from a reactive signal. This happens entirely at compile time, removing the need for runtime bundling or translation.

## Signal Serialization and Hydration

State management relies on the `Signal<T>` type defined in [`crates/topcoat-runtime/src/signal.rs`](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat-runtime/src/signal.rs). When a component declares `signal count = 0.0`, the server creates a `Signal` instance and serializes its initial value into an HTML comment:

```html
<!--::topcoat::signal({"id":"count-0","value":0})-->

```

The runtime script scans for these comments during hydration, reconstructs the signals as JavaScript objects, and stores them in a reactive map. This bridges server-rendered state with client-side interactivity without JSON endpoints or additional network requests.

## Reactive DOM Updates Without Network Round-Trips

Runtime expressions register as *watchers* on the signals they access. When code calls `.set()`, `.increment()`, or other mutators, the runtime re-evaluates dependent expressions and patches affected DOM nodes immediately.

The client-side implementation mirrors the server’s view hierarchy, ensuring updates happen locally:

```rust
view! {
    signal count = 0.0;
    <button @click=$(|_e| count.increment())>"+"</button>
    <p>"Current: " $(count.get())</p>
}

```

Here, clicking the button triggers the increment handler (compiled to JavaScript), which updates the signal and causes the paragraph text to refresh instantly.

## Event Handling and Bind Attributes

Interactive attributes use prefixes to distinguish client behavior. Attributes starting with `@` become event listeners whose handlers are generated by `expr!`. The `@click` attribute in the previous example compiles to a native JavaScript click listener.

Bind attributes prefixed with `:` synchronize DOM properties with signal values:

```rust
view! {
    signal open = false;
    <button @click=$(|_e| open.toggle())>"Toggle"</button>
    <section :hidden=$(!open.get())>"Hidden when closed"</section>
}

```

The runtime keeps the `hidden` attribute in sync with the negated signal value, updating it reactively whenever `open` changes.

## Calling Server Procedures from the Client

When client-side state requires server validation or computation, handlers can `await` async procedures. The runtime script performs the HTTP request while the UI remains responsive:

```rust
#[procedure]
async fn double(v: f64) -> Result<f64> { Ok(v * 2.0) }

view! {
    signal n = 1.0;
    <button @click=$(async |_e| {
        let doubled = double(n.get()).await?;
        n.set(doubled);
    })>"Double"</button>
}

```

This pattern combines local reactivity with server communication without requiring separate API routes or manual fetch implementations.

## Summary

- **Single asset delivery**: The `topcoat::runtime::script()` function injects the only JavaScript file needed, located at [`crates/topcoat-runtime/browser/dist/index.js`](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat-runtime/browser/dist/index.js).
- **Macro-generated JavaScript**: The `expr!` macro in `crates/topcoat-runtime/grammar/src/expr/` transforms Rust expressions into executable JavaScript during compilation.
- **Comment-based hydration**: Signals serialize to HTML comments like `::topcoat::signal({...})`, which the runtime script hydrates into reactive JavaScript objects.
- **Direct DOM patching**: Signal mutations trigger immediate DOM updates through locally registered watchers, eliminating network latency for UI changes.
- **Zero external tooling**: No bundlers, WebAssembly, or build pipelines are required beyond the Rust compiler.

## Frequently Asked Questions

### How does Topcoat differ from WebAssembly-based frameworks?

Topcoat generates plain JavaScript through compile-time macros rather than compiling Rust to WebAssembly. According to the source code in `crates/topcoat-runtime/grammar/src/expr/`, the `expr!` macro emits JavaScript snippets directly, allowing the browser to execute native code without loading a WASM module or glue code.

### Where is the client-side JavaScript actually stored?

The runtime script lives as a static byte array in [`crates/topcoat-runtime/src/lib.rs`](https://github.com/tokio-rs/topcoat/blob/main/crates/topcoat-runtime/src/lib.rs), embedded using `include_bytes!("../browser/dist/index.js")`. This asset is served by the Axum router when you add `AssetBundle::load()`, making it available at a predictable URL that `topcoat::runtime::script()` references.

### Can I use existing JavaScript libraries with Topcoat?

Since Topcoat injects a single script tag and generates vanilla JavaScript expressions, you can interface with global browser APIs or external scripts loaded via standard `<script>` tags. However, the reactive system specifically manages state through Topcoat's `Signal` type, so third-party reactive libraries may require wrapper functions to integrate with the signal comment format.

### What happens if JavaScript is disabled?

Topcoat applications are server-rendered by default. Without JavaScript, users receive fully functional HTML with initial signal values rendered inline. The reactive enhancements degrade gracefully to static content, maintaining accessibility and SEO benefits while losing client-side interactivity.