Skip to content

Signals

wirelessEye edited this page Jun 30, 2026 · 4 revisions

Signals

The nestix-signal crate provides the reactive runtime used by Nestix. The main nestix crate re-exports these APIs.

State

Create mutable reactive state with create_state.

let count = create_state(0);

count.set(1);
count.update(|value| value + 1);
count.mutate(|value| *value += 1);

Common methods:

  • borrow(): borrow the current value and track the read.
  • get(): clone the current value and track the read.
  • set(value): replace the value and notify dependents only if it changed.
  • set_unchecked(value): replace and always notify.
  • update(|prev| next): compute a replacement from the previous value.
  • mutate(|value| ...): mutate in place and notify.
  • into_readonly(): expose the state as Readonly<T>.

set requires T: PartialEq. get requires T: Clone.

Batching

Use batch to group multiple updates and rerun dependent effects once after the outermost batch finishes.

batch(|| {
    count.set(1);
    count.set(2);
});

Computed values are still invalidated immediately, so reading a computed value inside the batch returns the current value.

Computed

Computed values are lazy, cached derivations of other signals.

let count = create_state(0);
let label = computed!([count] || format!("Count: {}", count.get()));

assert_eq!(label.get(), "Count: 0");

The first read runs the closure and caches the result. When any signal read by the closure changes, the computed value is marked dirty. The next read recomputes it.

Use the function form when you do not need capture cloning:

let doubled = nestix::computed(move || count.get() * 2);

Use the macro form for ergonomic cloning:

let doubled = computed!([count] || count.get() * 2);

Effects

Effects run immediately and rerun whenever a signal they read changes.

let count = create_state(0);

effect!([count] || {
    log::debug!("count is {}", count.get());
});

Each effect refreshes its dependency list every time it runs. If an effect reads different signals after a branch changes, only the latest reads remain tracked.

Keep the returned handle when an effect should be stopped later:

let handle = effect!([count] || {
    log::debug!("count is {}", count.get());
});

handle.cancel();

Dropping the handle does not cancel the effect.

Use scoped_effect when an effect should automatically stop after an element unmounts:

scoped_effect(element, closure!([count] || {
    log::debug!("count is {}", count.get());
}));

The macro form accepts the element first, followed by the same closure syntax as effect!:

scoped_effect!(element, [count] || {
    log::debug!("count is {}", count.get());
});

Readonly

Readonly<T> wraps any signal and exposes only get.

let editable = create_state(String::new());
let readonly = editable.clone().into_readonly();

This is useful for list items and child APIs that should react to updates without mutating the original state.

Untracked Reads

Use untrack when a read should not subscribe the current effect or computed value.

let snapshot = untrack(|| count.get());

Nestix uses this internally when mounting newly created list children so that mounting work does not accidentally become a dependency of the surrounding list effect.

Shared

Shared<T> is a small Rc<T> wrapper that compares and hashes by pointer identity. It is used for callbacks, host handles, dependency sets, and other values that need stable identity.

let callback: Shared<dyn Fn()> = callback!(|| log::info!("clicked"));

Shared<dyn Any> can be downcast:

if let Ok(button) = handle.clone().downcast::<HtmlElement>() {
    // use button
}

Debug Configuration

debug_signals configures debug-only checks. In release builds it has no effect.

debug_signals(DebugConfig {
    detect_cyclic: true,
});

When detect_cyclic is enabled in debug builds, the runtime logs a warning if an effect tries to trigger itself while it is already running.

Clone this wiki locally