Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Reactive Model

Guido uses a fine-grained reactive system inspired by SolidJS. This enables efficient updates where only the affected parts of the UI change.

RwSignal (Read-Write)

create_signal() returns an RwSignal<T> — a read-write reactive value (8 bytes, Copy):

#![allow(unused)]
fn main() {
use guido::prelude::*;

let count = create_signal(0); // RwSignal<i32>

// Read the current value
let value = count.get();

// Set a new value
count.set(5);

// Update based on current value
count.update(|c| *c += 1);
}

Key Properties

  • Copy - RwSignal implements Copy, so you can use it in multiple closures without cloning
  • Background updates - Use .writer() to get a WriteSignal<T> for background task updates
  • Automatic tracking - Dependencies are tracked when reading inside reactive contexts
  • Converts to Signal - Call .read_only() or .into() to get a read-only Signal<T>

Signal (Read-Only)

Signal<T> is a read-only reactive value (16 bytes, Copy). It cannot be written to — calling .set() is a compile-time error. There are two ways to create one:

#![allow(unused)]
fn main() {
// Stored: wraps a static value
let name = create_stored("hello".to_string()); // Signal<String>

// Derived: closure-backed, re-evaluates on each read
let doubled = create_derived(move || count.get() * 2); // Signal<i32>
}

You can also convert an RwSignal to a Signal:

#![allow(unused)]
fn main() {
let count = create_signal(0);
let read_only: Signal<i32> = count.read_only(); // or count.into()
}

Memos

Eagerly computed values that automatically update when their dependencies change. Memos only notify downstream subscribers when the result actually differs (PartialEq), preventing unnecessary updates:

#![allow(unused)]
fn main() {
let count = create_signal(0);
let doubled = create_memo(move || count.get() * 2);

count.set(5);
println!("{}", doubled.get()); // Prints: 10
}

Memos are Copy like signals and can be used directly as widget properties:

#![allow(unused)]
fn main() {
let label = create_memo(move || format!("Count: {}", count.get()));
text(label)  // Only repaints when the formatted string changes
}

Effects

Side effects that re-run when tracked signals change:

#![allow(unused)]
fn main() {
let name = create_signal("World".to_string());

create_effect(move || {
    println!("Hello, {}!", name.get());
});

name.set("Guido".to_string()); // Effect re-runs, prints: Hello, Guido!
}

Effects are useful for logging, syncing with external systems, or triggering actions.

Using Signals in Widgets

Most widget properties accept either static values or reactive sources:

Static Value

#![allow(unused)]
fn main() {
container().background(Color::RED)
}

Signal

#![allow(unused)]
fn main() {
let bg = create_signal(Color::RED);
container().background(bg)
}

Closure

#![allow(unused)]
fn main() {
let is_active = create_signal(false);
container().background(move || {
    if is_active.get() { Color::GREEN } else { Color::RED }
})
}

Reactive Text

Text content can be reactive using closures:

#![allow(unused)]
fn main() {
let count = create_signal(0);

text(move || format!("Count: {}", count.get()))
}

The text automatically updates when count changes.

The IntoSignal Pattern

Widget properties accept Signal<T> via the IntoSignal<T> trait. Both RwSignal<T> and Signal<T> work, along with raw values and closures:

  • Static values → creates a stored Signal<T> via create_stored()
  • Closures → creates a derived Signal<T> via create_derived()
  • Signal<T> → passed through directly
  • RwSignal<T> → automatically converted to Signal<T>

You don’t need to create signals manually for widget properties — just pass values, closures, or signals directly.

Per-Field Signals

When multiple widgets depend on different fields of the same struct, #[derive(SignalFields)] generates per-field signals so each widget only re-renders when its specific field changes:

#![allow(unused)]
fn main() {
#[derive(Clone, PartialEq, SignalFields)]
pub struct AppState {
    pub cpu: f64,
    pub memory: f64,
    pub title: String,
}

// Creates individual Signal<T> for each field
let state = AppStateSignals::new(AppState {
    cpu: 0.0, memory: 0.0, title: "App".into(),
});

// Each widget subscribes to only the field it reads
text(move || format!("CPU: {:.0}%", state.cpu.get()))
text(move || format!("MEM: {:.0}%", state.memory.get()))
text(move || state.title.get())
}

Use .writers() to get Send handles for background task updates:

#![allow(unused)]
fn main() {
let writers = state.writers();

let _ = create_service::<(), _, _>(move |_rx, ctx| async move {
    while ctx.is_running() {
        // Each field is set individually with PartialEq change detection.
        // Effects that depend on multiple fields run only once (batched).
        writers.set(AppState {
            cpu: read_cpu(),
            memory: read_memory(),
            title: get_title(),
        });
        tokio::time::sleep(Duration::from_secs(1)).await;
    }
});
}

Generic structs are supported — the generated types carry the same generic parameters:

#![allow(unused)]
fn main() {
#[derive(Clone, PartialEq, SignalFields)]
pub struct Pair<A: Clone + PartialEq + Send + 'static, B: Clone + PartialEq + Send + 'static> {
    pub first: A,
    pub second: B,
}

let pair = PairSignals::new(Pair { first: 1i32, second: "hello".to_string() });
}

Untracked Reads

Sometimes you want to read a signal without creating a dependency:

#![allow(unused)]
fn main() {
let count = create_signal(0);

// Normal read - creates dependency
let value = count.get();

// Untracked read - no dependency
let value = count.get_untracked();
}

This is useful in effects where you want to read initial values without re-running on changes.

Ownership & Cleanup

Signals and effects created inside dynamic children are automatically cleaned up when the child is removed. Use on_cleanup to register custom cleanup logic:

#![allow(unused)]
fn main() {
container().children(move || {
    items.get().into_iter().map(|id| (id, move || {
        // These are automatically owned and disposed
        let count = create_signal(0);
        create_effect(move || println!("Count: {}", count.get()));

        // Register custom cleanup for non-reactive resources
        on_cleanup(move || {
            println!("Child {} removed", id);
        });

        container().child(text(move || count.get().to_string()))
    }))
})
}

See Dynamic Children for more details on automatic ownership.

Best Practices

Read Close to Usage

Read signals where the value is needed, not at the top of functions:

#![allow(unused)]
fn main() {
// Good: Read in closure where it's used
text(move || format!("Count: {}", count.get()))

// Less optimal: Read early, pass static value
let value = count.get();
text(format!("Count: {}", value))  // Won't update!
}

Use Context for App-Wide State

For values that many widgets across different modules need (config, theme, services), use the Context API instead of passing signals through every function:

#![allow(unused)]
fn main() {
// Setup
provide_context(Config::load());

// Any widget, any module
let cfg = expect_context::<Config>();
}

For mutable shared state, use provide_signal_context to combine context with reactivity.

Use Memo for Derived State

Instead of manually syncing values:

#![allow(unused)]
fn main() {
// Bad: Manual sync
let count = create_signal(0);
let doubled = create_signal(0);
// Must remember to update doubled when count changes

// Good: Use memo
let count = create_signal(0);
let doubled = create_memo(move || count.get() * 2);
}

API Reference

Signal Creation

#![allow(unused)]
fn main() {
pub fn create_signal<T: Clone + PartialEq + Send + 'static>(value: T) -> RwSignal<T>;
pub fn create_stored<T: Clone + 'static>(value: T) -> Signal<T>;
pub fn create_derived<T: Clone + 'static>(f: impl Fn() -> T + 'static) -> Signal<T>;
pub fn create_memo<T: Clone + PartialEq + 'static>(f: impl Fn() -> T + 'static) -> Memo<T>;
pub fn create_effect(f: impl Fn() + 'static);
}

RwSignal Methods

#![allow(unused)]
fn main() {
impl<T: Clone> RwSignal<T> {
    pub fn get(&self) -> T;           // Read with tracking
    pub fn get_untracked(&self) -> T; // Read without tracking
    pub fn set(&self, value: T);      // Set new value
    pub fn update(&self, f: impl FnOnce(&mut T)); // Update in place
    pub fn writer(&self) -> WriteSignal<T>; // Get Send handle for background threads
    pub fn read_only(&self) -> Signal<T>;   // Convert to read-only Signal
}
}

Signal Methods

#![allow(unused)]
fn main() {
impl<T: Clone> Signal<T> {
    pub fn get(&self) -> T;           // Read with tracking
    pub fn get_untracked(&self) -> T; // Read without tracking
    pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R; // Borrow with tracking
    pub fn with_untracked<R>(&self, f: impl FnOnce(&T) -> R) -> R; // Borrow without tracking
    // No set/update/writer — Signal is read-only
}
}

Memo Methods

#![allow(unused)]
fn main() {
impl<T: Clone + PartialEq> Memo<T> {
    pub fn get(&self) -> T;           // Read with tracking
    pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R; // Borrow with tracking
}
}

Cleanup

#![allow(unused)]
fn main() {
// Register cleanup callback (for use in dynamic children)
pub fn on_cleanup(f: impl FnOnce() + 'static);
}

Background Services

#![allow(unused)]
fn main() {
// Create an async background service with automatic cleanup
pub fn create_service<Cmd, F, Fut>(f: F) -> Service<Cmd>
where
    Cmd: Send + 'static,
    F: FnOnce(UnboundedReceiver<Cmd>, ServiceContext) -> Fut + Send + 'static,
    Fut: Future<Output = ()> + Send + 'static;
}

See Background Tasks for detailed usage.