name: dwind-reactivity description: Use when the user asks about state management, signals, Mutable, reactive updates, conditional rendering, signal composition, child_signal, style_signal, broadcast, map_ref, or encounters signal-related compile errors in a dwind/dominator/futures-signals context. version: 1.0.0

Dwind Reactivity — Signals & State Management

The dwind stack uses futures-signals for fine-grained reactivity. State lives in Mutable<T> values; the DOM subscribes to changes via signals.

Mutable

#![allow(unused)]
fn main() {
use futures_signals::signal::Mutable;

let count = Mutable::new(0);
count.set(5);               // Set value
let val = count.get();      // Get current value
let sig = count.signal();   // Get a signal (for primitives implementing Copy)
let sig = count.signal_cloned(); // For non-Copy types (String, Vec, etc.)
let sig = count.signal_ref(|v| v.len()); // Map reference without cloning
}

Signal Consumption Rule

A signal can only be consumed once (.map() takes ownership). If you need the same signal in multiple places, use .broadcast():

#![allow(unused)]
fn main() {
let disabled = disabled.broadcast();

// Now call .signal() as many times as needed
.style_signal("opacity", disabled.signal().map(|d| if d { "0.5" } else { "1" }))
.attr_signal("disabled", disabled.signal().map(|d| if d { Some("disabled") } else { None }))
.attr_signal("aria-disabled", disabled.signal().map(|d| if d { Some("true") } else { None }))
}

If you forget .broadcast() and use a signal twice, you get a move error.

DOM Bindings

text_signal — Reactive text

#![allow(unused)]
fn main() {
.text_signal(count.signal().map(|n| format!("Count: {}", n)))
}

child_signal — Conditional DOM (returns Option)

#![allow(unused)]
fn main() {
.child_signal(is_open.signal().map(|open| {
    if open { Some(html!("div", { .text("Panel content") })) } else { None }
}))
}

style_signal — Reactive inline styles

#![allow(unused)]
fn main() {
.style_signal("opacity", is_visible.signal().map(|v| if v { "1" } else { "0" }))
}

attr_signal — Reactive attributes (returns Option<&str>)

#![allow(unused)]
fn main() {
.attr_signal("disabled", disabled.signal().map(|d| if d { Some("disabled") } else { None }))
}

visible_signal — Show/hide via CSS display

#![allow(unused)]
fn main() {
.visible_signal(is_visible.signal())
}

dwclass_signal! — Reactive utility classes

#![allow(unused)]
fn main() {
.dwclass_signal!("bg-blue-500", is_active.signal())
}

Combining Signals with map_ref!

When a value depends on multiple signals:

#![allow(unused)]
fn main() {
use futures_signals::map_ref;

.style_signal("box-shadow", {
    map_ref! {
        let valid = is_valid.signal(),
        let focused = is_focused.signal() => {
            if !*valid { "var(--shadow-error)" }
            else if *focused { "var(--shadow-focus)" }
            else { "var(--shadow)" }
        }
    }
})
}

SignalExt Combinators

#![allow(unused)]
fn main() {
use futures_signals::signal::SignalExt;

signal.map(|v| v + 1)           // Transform
not(bool_signal)                 // Negate
and(sig_a, sig_b)                // Logical AND
or(sig_a, sig_b)                 // Logical OR
signal.for_each(|v| async { })  // Side effect
signal.boxed_local()             // Type-erase for trait objects
}

Reactive Lists

#![allow(unused)]
fn main() {
use futures_signals::signal_vec::{MutableVec, SignalVecExt};

let items = MutableVec::new();
items.lock_mut().push_cloned("new item".to_string());

html!("ul", {
    .children_signal_vec(items.signal_vec_cloned().map(|item| {
        html!("li", { .text(&item) })
    }))
})
}

Programmatic Responsive Behavior

#![allow(unused)]
fn main() {
use dwind::prelude::media_queries::{breakpoint_active_signal, Breakpoint};

let is_desktop = breakpoint_active_signal(Breakpoint::Medium);

html!("div", {
    .child_signal(is_desktop.map(|desktop| {
        if desktop { Some(desktop_nav()) } else { Some(mobile_nav()) }
    }))
})
}

Decision Tree: Which Reactive Binding?

  • Structural changes (add/remove DOM nodes): child_signal / children_signal_vec
  • Visual changes (colors, opacity, size): dwclass_signal! / style_signal
  • Simple show/hide: visible_signal (keeps DOM alive, toggles display)
  • Text updates: text_signal
  • Attribute changes: attr_signal

Critical Gotchas

style_signal must NEVER return empty string

Dominator panics in debug builds on empty style values:

#![allow(unused)]
fn main() {
// BAD — panics when size isn't Small
.style_signal("border-radius", size.signal().map(|s| match s {
    Size::Small => "4px",
    _ => "",  // PANIC!
}))

// GOOD — every branch returns a valid CSS value
.style_signal("border-radius", size.signal().map(|s| match s {
    Size::Small => "4px",
    Size::Medium => "8px",
    Size::Large => "12px",
}))
}

Vendor-prefixed CSS needs array syntax

#![allow(unused)]
fn main() {
// BAD — panics if browser doesn't support prefix
.style("-webkit-backdrop-filter", "blur(8px)")

// GOOD — tries each name, succeeds if any works
.style(["backdrop-filter", "-webkit-backdrop-filter"], "blur(8px)")
}

CSS visibility vs conditional DOM destruction

Prefer CSS visibility over child_signal when content has its own signals:

#![allow(unused)]
fn main() {
// PROBLEMATIC — content signal consumed on creation, destroyed on close, can't recreate
.child_signal(open.signal().map(move |is_open| {
    if is_open { Some(panel_with_content_signal) } else { None }
}))

// BETTER — panel always in DOM, CSS controls visibility
.child(html!("div", {
    .style_signal("opacity", open.signal().map(|o| if o { "1" } else { "0" }))
    .style_signal("pointer-events", open.signal().map(|o| if o { "auto" } else { "none" }))
    .child_signal(content)  // consumed once, lives forever
}))
}

Never use return inside map_ref!

The macro expansion makes return exit the wrong scope:

#![allow(unused)]
fn main() {
// BAD — type mismatch with Poll
map_ref! { let a = sig => { if *a { return "yes"; } "no" } }

// GOOD — use if/else expression
map_ref! { let a = sig => { if *a { "yes" } else { "no" } } }
}

Box<dyn Fn()> is not Clone — use Rc

#![allow(unused)]
fn main() {
let on_close = std::rc::Rc::new(on_close);
.event({ let on_close = on_close.clone(); move |_: events::Click| { (on_close)(); } })
.global_event({ let on_close = on_close.clone(); move |e: events::KeyDown| {
    if e.key() == "Escape" { (on_close)(); }
}})
}

Use explicit style signals for disabled state on <label> elements:

#![allow(unused)]
fn main() {
.style_signal("opacity", disabled.signal().map(|d| if d { "0.5" } else { "1" }))
.style_signal("pointer-events", disabled.signal().map(|d| if d { "none" } else { "auto" }))
}