Reactivity
Reactivity
Section titled “Reactivity”Use reactive state when a value change should update UI. Use refs when a value should persist without rendering.
Create component-owned signals, stores, memos, refs, effects, and watchers in Component::create and retain the handles. These constructors do not reuse a call slot on each render; repeatedly calling them in render creates new state or subscriptions. Element refs and the async render hooks have separate retained-slot behavior.
Signals
Section titled “Signals”Signal<T> is the basic reactive cell.
let count = ctx.signal(0);
let value = count.get();count.set(value + 1);count.update(|value| *value += 1);Reads through get() and with() are tracked by memos and effects. Reads through get_untracked() and with_untracked() are not.
let name = signal.with(|value| value.name.clone());let current = signal.get_untracked();When a signal created with ctx.signal(...) changes, the owning component context is marked dirty. The next pass rerenders that component subtree.
Signal ownership is component-scoped. If one component owns a: Signal<T> and b: Signal<U> and reads a during render, changing a rerenders that component. Plain elements created in that render, including elements bound to b, are rebuilt as new Node descriptions and then reconciled against the previous retained tree.
Mounted child components form their own retained slots. During a parent rerender, ctx.mount::<Child>(props) reuses the existing slot when the component type/key still match. If the child props, context, slot children, and the child’s own dirty state are unchanged, the child’s render method is skipped and its previous rendered output is cloned for reuse.
DevTools Type Bound
Section titled “DevTools Type Bound”Without the devtools feature, any T can be a signal value.
With devtools enabled, Signal<T> requires T: DevtoolsInspectable. This is intentional: DevTools can show live signal values without storing extra debug data in non-devtools builds.
#[derive(Clone, PartialEq, lurq::DevtoolsInspectable)]struct CounterState { count: i32,}
let state = ctx.signal(CounterState { count: 0 });Use #[devtools_ignore] on fields that should not be displayed.
#[derive(Clone, PartialEq, lurq::DevtoolsInspectable)]struct Session { user: String, #[devtools_ignore] token: String,}Stores And Lenses
Section titled “Stores And Lenses”Store<T> wraps structured state in a signal. Use it when a component owns a larger model.
#[derive(Clone, PartialEq, lurq::DevtoolsInspectable)]struct User { name: String, age: u32,}
let user = ctx.store(User { name: "Ada".into(), age: 36 });user.update(|user| user.age += 1);Use lens to expose one field to child code.
let name = user.lens( |user| user.name.clone(), |user, name| user.name = name,);
name.set("Grace".into());Lens has get, set, and update.
ctx.memo(...) creates derived reactive state. The closure runs once immediately, tracks signals read inside it, and recomputes when those dependencies change.
let count = ctx.signal(0);let doubled = ctx.memo({ let count = count.clone(); move || count.get() * 2});
let value = doubled.get();Memos only publish when the new value is different from the old value, so T must implement PartialEq.
ctx.create_ref(...) creates persistent non-reactive state.
let render_count = ctx.create_ref(0_u64);render_count.update(|count| *count += 1);Ref updates do not mark the component dirty. Use refs for handles, cached measurements, counters, and imperative coordination.
Effects
Section titled “Effects”ctx.on_effect(...) runs immediately and reruns when tracked values read by the closure change.
let count = ctx.signal(0);
ctx.on_effect({ let count = count.clone(); move || println!("count changed to {}", count.get())});Effects are retained by the component context and dropped when the context is dropped.
Watchers
Section titled “Watchers”ctx.watch(&signal, callback) subscribes directly to one signal.
ctx.watch(&count, |value| { println!("new count: {value}");});Use an effect when dependencies should be discovered from reads. Use a watcher when you already know the exact signal to observe.
Batch Updates
Section titled “Batch Updates”ctx.batch(...) groups updates so dirty marking is deferred until the closure completes.
ctx.batch(|| { first.set(1); second.set(2);});Writes From Callbacks
Section titled “Writes From Callbacks”Watcher callbacks, effects, and memos may write the signals they observe, subscribe new watchers, and drop subscriptions. lurq holds no lock while it calls them.
ctx.watch(&status, { let status = status.clone(); move |value| { if *value == Status::Failed { status.set(Status::Retrying); } }});A write inside a callback takes effect at once: get() returns the new value right after set. Its notification comes after the current one. lurq first delivers the current value to every observer of the signal, then notifies them all again with the latest value. Every observer sees the values in the same order. Several writes during one notification produce one more notification, carrying the last value. A write from another thread during a notification is handled the same way: the thread that is already notifying delivers it.
A watcher added during a notification starts with the next one. A watcher dropped during a notification is not called for the rest of it.
An effect that writes a signal it reads runs again with the new value, including when it writes during its first run.
set does not compare values, so a callback or effect that writes its own signal on every notification never settles. Compare before writing. lurq stops such a loop after 100 consecutive notifications caused by the signal’s own observers, and the set that started the notifications panics. The signal keeps working afterwards.
Inside a ctx.watch callback, use set on the watched signal, not update. The callback still borrows the value it received, so update on that signal panics there. update works from effects, from memos, and on other signals.
Do not write a signal inside its own with closure. That closure holds the signal’s read lock.
Static Context
Section titled “Static Context”Static context stores a cloned value by type.
#[derive(Clone)]struct Locale(&'static str);
ctx.provide(Locale("en-US"));Descendants read by type:
if let Some(locale) = ctx.use_context::<Locale>() { println!("{}", locale.0);}Use static context for values that do not need to notify consumers when changed.
A value provided in create stays provided for the provider’s lifetime: ancestor re-renders refresh the inherited
contexts and layer the provider’s own values back on top. A value provided in render lasts for that render and is
provided again by the next one, or removed if the next render does not provide it. See
Ctx for the details.
Reactive Context
Section titled “Reactive Context”Reactive context is a typed context value that can notify consumers.
#[derive(Clone, Hash)]struct ThemeName(&'static str);
let theme = ctx.create_context(ThemeName("dark"));theme.set(ThemeName("light"));Descendants consume it:
let theme = ctx.consume_context::<ThemeName>().unwrap();let current = theme.get();ReactiveContext<T> requires T: Clone + Hash + Send + Sync + 'static. Updates notify consumers only when the hash changes.
Common Patterns
Section titled “Common Patterns”For local component state, create signals in create.
For parent-controlled state, pass a Signal<T> through props.
For derived display state, use a memo.
For side effects, prefer on_effect over doing work directly in render.
For context, create or provide at a stable provider component and consume from descendants.