Files

8.1 KiB

Signals and State Management

Dioxus state management is built on a layered architecture: generational-box for memory, signals for reactivity, and stores for nested data.

Generational Box

Core Concept

Provides Copy semantics for references through generation-based validation:

GenerationalBox<T, S>
├── raw: GenerationalPointer<S>
│   ├── storage: &'static S
│   └── location: GenerationalLocation
│       ├── generation: NonZeroU64
│       └── created_at: &'static Location (debug)
└── _marker: PhantomData<T>

Memory Management

Allocation:

pub trait Storage<Data> {
    fn new(value: Data, caller: Location) -> GenerationalPointer<Self>;
    fn new_rc(value: Data, caller: Location) -> GenerationalPointer<Self>;
}

Storage Variants:

  • UnsyncStorage - Single-threaded, uses RefCell (fast, no locking)
  • SyncStorage - Multi-threaded, uses RwLock (thread-safe)

StorageEntry:

StorageEntry<Data>
├── generation: NonZeroU64
├── refcount: u32
└── data: Data  // Empty, Data, Rc, or Reference

Generation Tracking

  1. Caller provides pointer with generation X
  2. Storage checks: entry.generation == X
  3. If yes: data valid, proceed
  4. If no: return BorrowError::Dropped

When dropped: generation incremented, data cleared, existing pointers invalidated.

Owner Pattern

pub struct Owner<S> {
    owned: Vec<GenerationalPointer<S>>,
}

impl<S> Drop for Owner<S> {
    fn drop(&mut self) {
        for location in self.owned.drain(..) {
            location.recycle();  // Invalidates all pointers
        }
    }
}

Signals

Architecture

Signal<T, S>
└── inner: CopyValue<SignalData<T>, S>
    └── SignalData<T>
        ├── value: T
        └── subscribers: Arc<Mutex<HashSet<ReactiveContext>>>

Signal Types

Signal<T, S> - Primary mutable reactive primitive:

  • Implements Readable for subscribed reads
  • Implements Writable for reactive updates
  • .read() subscribes current scope
  • .peek() reads without subscribing

Memo - Derived reactive value:

  • Wraps Signal<T> for value storage
  • Contains UpdateInformation tracking dirty state
  • Lazy evaluation: only recomputes when dependencies change
  • Uses PartialEq to skip updates if unchanged

CopyValue<T, S> - Generic mutable wrapper:

  • Lower-level than Signal
  • No built-in reactivity
  • Building block for Signal

Global<T, R> - Lazy singleton:

  • Created once per app on first access
  • Stored in ScopeId::ROOT context
  • Uses InitializeFromFunction trait

ReadSignal / WriteSignal - Type-erased boxed signals:

  • Store Box<dyn DynReadable>
  • Flexible APIs accepting any readable type

MappedSignal<O, V, F> - Derived readonly:

  • Maps inner signal through function F
  • signal.map(|x| &x.field)

Reactivity System

Automatic Subscription:

signal.read()
  → try_read_unchecked()
  → Check ReactiveContext::current()
  → If exists: reactive_context.subscribe(signal.subscribers)
  → Later writes call mark_dirty() on all subscribers

Update Propagation:

signal.write()
  → WriteLock created
  → User modifies value
  → WriteLock dropped → SignalSubscriberDrop::drop()
  → signal.update_subscribers():
    → Get subscriber snapshot (brief lock)
    → Call mark_dirty() on each
    → Re-extend subscriber list

Global Signals

GlobalLazyContext:

GlobalLazyContext
└── map: Rc<RefCell<HashMap<GlobalKey, Box<dyn Any>>>>

Resolution:

  1. Static const defines GlobalSignal<T>
  2. First access calls resolve():
    • Check HashMap by key
    • If found: return clone
    • If not: run constructor in ROOT scope
    • Store result
  3. Subsequent accesses return same instance

Key Types:

  • GlobalKey::File { file, line, column, index }
  • GlobalKey::Raw(&'static str)

Hooks

Core Hook Pattern

#[track_caller]
pub fn use_hook<T>(f: impl FnOnce() -> T) -> T {
    let component_id = current_scope_id();
    let mut hooks = get_hooks(component_id);

    if hooks.len() <= hook_index {
        hooks.push(Box::new(f()));  // First render
    }
    hooks[hook_index].clone()  // Return existing
}

Hook Types

use_signal() -> Signal

  • Creates local signal owned by component
  • State persists across renders

use_memo() -> Memo

  • Creates memoized computation
  • Reruns when read signals change

use_effect(callback)

  • Runs side effects when dependencies change
  • Creates ReactiveContext to track reads
  • Queues effect rerun on dependency notification

use_resource<T, F>(future_fn) -> Resource

  • Async state management
  • Watches dependencies, reruns future when they change
  • Returns Resource<T> with value, state, task

use_callback<I, O>() -> Callback<I, O>

  • Memoized callback
  • Prevents unnecessary closures

use_coroutine(init)

  • Long-lived task
  • Spawned once at component creation

use_context() -> T

  • Retrieves ancestor context

Hook Rules

  1. Must call same hooks every render
  2. Must call in same order
  3. Order determines hook identity
  4. Breaking rules causes panic or bugs

Stores

Purpose

Signals work for scalar state. Stores provide:

  • Granular reactivity per field
  • Lazy signal creation
  • Ergonomic field access

Store Architecture

Store<T, Lens>
└── selector: SelectorScope<Lens>
    ├── subscriptions: StoreSubscriptions
    ├── path: TinyVec<u16>
    └── value: Lens

Subscription Tree

StoreSubscriptions
└── inner: CopyValue<StoreSubscriptionsInner>
    └── root: SelectorNode
        ├── subscribers: HashSet<ReactiveContext>
        └── root: HashMap<PathKey, SelectorNode>
            └── [0] → SelectorNode
            └── [1] → SelectorNode

Path Tracking

let store = Store::new(vec![a, b, c]);
let item_1 = store[1];    // path = [1]
let field = item_1.name;  // path = [1, field_hash]

// Writing store[1].name only marks dirty:
// - subscribers at path [1]
// - subscribers at path [1, field_hash]
// - NOT path [0] or [2]

Store Macro

#[derive(Store)]
struct TodoItem {
    checked: bool,
    contents: String,
}

// Generates:
pub trait TodoItemStoreExt<__Lens> {
    fn checked(self) -> Store<bool, __Lens::MappedSignal>;
    fn contents(self) -> Store<String, __Lens::MappedSignal>;
    fn transpose(self) -> TodoItemStoreTransposed;
}

pub struct TodoItemStoreTransposed {
    pub checked: Store<bool>,
    pub contents: Store<String>,
}

Enum Support

#[derive(Store)]
enum Status {
    Loading,
    Ready(String),
    Error(String),
}

// Generates:
fn is_loading(self) -> bool;
fn ready(self) -> Option<Store<String, ...>>;
fn transpose(self) -> StatusStoreTransposed;

Key Traits

Readable

pub trait Readable {
    type Target;
    type Storage;
    fn try_read_unchecked(&self) -> Result<ReadableRef<T>>;
    fn try_peek_unchecked(&self) -> Result<ReadableRef<T>>;
    fn subscribers(&self) -> Subscribers;
}

Writable

pub trait Writable: Readable {
    type WriteMetadata;
    fn try_write_unchecked(&self) -> Result<WritableRef<T>>;
}

Storage (AnyStorage)

pub trait AnyStorage {
    type Ref<'a, T>: Deref<Target = T>;
    type Mut<'a, T>: DerefMut<Target = T>;
    fn map<T, U>(ref_: Ref<T>) -> Ref<U>;
    fn map_mut<T, U>(mut_ref: Mut<T>) -> Mut<U>;
}

Comparison

Feature Signal Memo Store
Mutability Mutable Read-only Mutable
Dependencies None Tracks reads Implicit via paths
Granularity Single value Computation Per-field
When to use Direct state Derived values Nested structures
Subscriptions Simple HashSet Via context Tree structure
Performance O(1) update O(deps) recompute O(path) update

Memory Model

  1. Heap: Values in storage singletons per-scope
  2. Stack: Only pointers (GenerationalBox) in components
  3. Cleanup: Owner drops when scope dies
  4. Validity: Generation checking on each access
  5. Sharing: Arc+Mutex for subscribers, Rc+RefCell for ownership