#signal #animation #run-time #remember #garbage-collection #widgets #desktop #webview #scoped-effect #ui-composition

repose-core

Repose's core runtime, view model, signals, composition locals, and animation clock

22 releases (10 breaking)

Uses new Rust 2024

0.13.1 Feb 12, 2026
0.12.2 Jan 25, 2026
0.8.3 Dec 14, 2025
0.7.1 Nov 18, 2025

#989 in GUI


Used in 9 crates

GPL-3.0-or-later

98KB
3K SLoC

State, Signals, and Effects

Repose uses a small reactive core instead of an explicit widget tree with mutable fields. There are three main pieces:

  • Signal<T> — observable, reactive value.
  • remember* — lifecycle‑aware storage bound to composition.
  • effect / scoped_effect — side‑effects with cleanup.

Signals

Signal<T> is a cloneable handle to a piece of state:

use repose_core::*;

let count = signal(0);
count.set(1);
count.update(|v| *v += 1);
assert_eq!(count.get(), 2);

Reads participate in a dependency graph: when you call get() inside an observer or produce_state, future writes will automatically recompute that observer.

Remembered state

UI state is typically held in remember_* slots rather than globals:

use repose_core::*;

fn CounterView() -> View {
    let count = remember_state(|| 0); // Rc<RefCell<i32>>

    let on_click = {
        let count = count.clone();
        move || *count.borrow_mut() += 1
    };

    repose_ui::Button(
        format!("Count = {}", *count.borrow()),
        on_click,
    )
}
  • remember and remember_state are order‑based: the Nth call in a composition slot always refers to the Nth stored value.
  • remember_with_key and remember_state_with_key are key‑based and more stable across conditional branches.

Derived state

produce_state computes a Signal<T> from other signals and recomputes it automatically when dependencies change:

let first = signal("Jane".to_string());
let last  = signal("Doe".to_string());

let full = produce_state("full_name", {
    let first = first.clone();
    let last  = last.clone();
    move || format!("{} {}", first.get(), last.get())
});

assert_eq!(full.get(), "Jane Doe");

Effects and cleanup

Use effect / scoped_effect for one‑off side‑effects with cleanups:

use repose_core::*;

fn Example() -> View {
    scoped_effect(|| {
        log::info!("Mounted Example");
        on_unmount(|| log::info!("Unmounted Example"))
    });

    // ...
    repose_ui::Box(Modifier::new())
}
  • effect runs once when the view is composed and returns a Dispose guard that will be run when the scope is torn down.
  • scoped_effect is wired to the current Scope and is cleaned up on scope disposal (e.g. when a navigation entry is popped).

For long‑running tasks (network, timers), prefer building small helpers on top of scoped_effect so everything cleans up correctly when the UI that owns it disappears.

Dependencies

~1.9–2.7MB
~52K SLoC