How to Use Signals for Client-Side State Management in Topcoat

Declare signals in your view! blocks using the signal keyword, read them by referencing their identifier in templates, and mutate them using methods like set() or update() inside client-side event handlers to trigger automatic UI updates.

In the tokio-rs/topcoat framework, signals provide a lightweight, reactive mechanism for managing UI state exclusively in the browser. Understanding how to use signals for client-side state management in Topcoat allows you to build interactive interfaces without writing JavaScript, leveraging Rust's type safety throughout your application.

Declaring Signals in View Macros

Signals are declared inside view! or page! blocks using the signal keyword followed by an initial value. This creates a Signal<T> where T is inferred from the provided value.

In examples/runtime/src/counter.rs, a numeric signal is declared as follows:

view! {
    // Creates a Signal<f64> with initial value 0.0
    signal count = 0.0;
    
    <p>"Current count: {count}"</p>
}

The signal becomes available throughout the enclosing view block and can hold any Rust type that implements the necessary serialization traits for cross-boundary communication.

Reading and Interpolating Signal Values

Reading a signal's current value requires only referencing its identifier. Topcoat automatically subscribes the surrounding template to changes, ensuring the DOM updates when the value mutates.

You can interpolate signals into text content, attributes, or conditional expressions:

view! {
    signal show = false;
    
    // Boolean signal controls visibility
    <div hidden={!show}>
        "Content appears when show is true"
    </div>
    
    <button on:click=|cx| { show.toggle(); }>
        "Toggle Visibility"
    </button>
}

Mutating Signals from Client-Side Code

Signals enforce a strict client-side mutation policy. You can only write to signals from within client-side event handlers or the expr! macro, never from server-side rendering contexts.

Available mutation methods include:

  • signal.set(new_value) — Replaces the current value entirely
  • signal.update(|v| ...) — Mutates the value using a closure
  • signal.toggle() — Available for boolean signals, flips the value

The following example from examples/runtime/src/counter.rs demonstrates incrementing a counter:

view! {
    signal count = 0.0;
    
    <p>"Current count: {count}"</p>
    
    <button on:click=|cx| {
        // Runs only in the browser
        count.update(|v| *v + 1.0);
    }>
        "Increment"
    </button>
}

For text inputs, bind the signal to the value attribute and update it on input events:

view! {
    signal input = String::new();
    
    <input type="text"
           value={input}
           on:input=|cx| {
               input.set(event.target().value());
           } />
    <p>"You typed: {input}"</p>
}

Automatic Reactivity and DOM Updates

When a signal's value changes, Topcoat automatically re-evaluates any dependent UI components without requiring a full page reload. This reactivity is implemented in crates/topcoat-runtime/src/signal.rs, where the Signal<T> type tracks subscriptions and notifies listeners upon mutation.

The framework compiles signal mutations to JavaScript that executes in the browser, maintaining a lightweight bridge between Rust's type system and the DOM.

Server-Side Restrictions and Safety

Topcoat strictly enforces that signals remain client-side constructs. Attempting to invoke write operations on a signal during server-side rendering triggers a panic.

As implemented in crates/topcoat-runtime/src/surrogate/signal.rs, the runtime panics with the message:

"expressions in which a signal is written to cannot be run server-side"

This design guarantees that signals never leak server-side state to the client inappropriately, and vice versa, maintaining clear boundaries between server-rendered HTML and client-side interactivity.

Using Signals with Shards

For components that require server-side re-rendering when client-side signals change, Topcoat provides the shard! macro. This pattern, demonstrated in examples/shard/src/main.rs, creates a server component that re-renders when bound signals update:

view! {
    signal input = String::new();
    
    <shard!>
        <p>"Live preview: {input}"</p>
    </shard!>
    
    <input type="text"
           value={input}
           on:input=|cx| input.set(event.target().value()) />
}

The shard! component receives the signal value from the client and re-renders on the server, bridging the gap between client-side state and server-side templates.

Summary

  • Declare signals using signal name = value; syntax inside view! blocks to create reactive Signal<T> values
  • Read signals by referencing their identifier in templates; changes trigger automatic DOM updates via the implementation in crates/topcoat-runtime/src/signal.rs
  • Mutate signals only from client-side handlers using set(), update(), or toggle() methods
  • Respect server boundaries — writing signals server-side panics as enforced by crates/topcoat-runtime/src/surrogate/signal.rs
  • Combine with shards to enable server-side re-rendering driven by client-side signal changes

Frequently Asked Questions

What happens if I try to write a signal on the server?

The application will panic with the message "expressions in which a signal is written to cannot be run server-side". This panic originates in crates/topcoat-runtime/src/surrogate/signal.rs and ensures signals remain strictly client-side constructs, preventing accidental server-side mutations of browser-only state.

Can I use custom Rust types with Topcoat signals?

Yes. Signals are generic over Signal<T>, allowing any Rust type that implements the necessary serialization traits. You can declare signal user = User::default(); or any custom struct, provided the type can be serialized for the client-server boundary.

How do signals differ from server-side state management in Topcoat?

Signals exist only in the browser and are not persisted on the server, whereas server-side state lives in your Rust application logic and renders HTML. Signals are ideal for ephemeral UI state like toggles, form inputs, and counters that do not require server persistence or database storage.

Do signals require manual JavaScript handling?

No. Topcoat compiles signal mutations to JavaScript automatically through the expr! macro and event handler syntax. You write Rust code using methods like signal.set() or signal.update(), and the framework generates the necessary browser-side JavaScript to execute these operations.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →