notes/architecture/04-SIGNALS.md
Dioxus state management is built on a layered architecture: generational-box for memory, signals for reactivity, and stores for nested data.
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>
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
entry.generation == XBorrowError::DroppedWhen dropped: generation incremented, data cleared, existing pointers invalidated.
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
}
}
}
Signal<T, S>
└── inner: CopyValue<SignalData<T>, S>
└── SignalData<T>
├── value: T
└── subscribers: Arc<Mutex<HashSet<ReactiveContext>>>
Signal<T, S> - Primary mutable reactive primitive:
Readable for subscribed readsWritable for reactive updates.read() subscribes current scope.peek() reads without subscribingMemo<T> - Derived reactive value:
Signal<T> for value storageUpdateInformation tracking dirty statePartialEq to skip updates if unchangedCopyValue<T, S> - Generic mutable wrapper:
Global<T, R> - Lazy singleton:
ScopeId::ROOT contextInitializeFromFunction traitReadSignal<T> / WriteSignal<T> - Type-erased boxed signals:
Box<dyn DynReadable>MappedSignal<O, V, F> - Derived readonly:
signal.map(|x| &x.field)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
GlobalLazyContext:
GlobalLazyContext
└── map: Rc<RefCell<HashMap<GlobalKey, Box<dyn Any>>>>
Resolution:
GlobalSignal<T>resolve():
Key Types:
GlobalKey::File { file, line, column, index }GlobalKey::Raw(&'static str)#[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
}
use_signal<T>() -> Signal<T>
use_memo<R>() -> Memo<R>
use_effect(callback)
ReactiveContext to track readsuse_resource<T, F>(future_fn) -> Resource<T>
Resource<T> with value, state, taskuse_callback<I, O>() -> Callback<I, O>
use_coroutine(init)
use_context<T>() -> T
Signals work for scalar state. Stores provide:
Store<T, Lens>
└── selector: SelectorScope<Lens>
├── subscriptions: StoreSubscriptions
├── path: TinyVec<u16>
└── value: Lens
StoreSubscriptions
└── inner: CopyValue<StoreSubscriptionsInner>
└── root: SelectorNode
├── subscribers: HashSet<ReactiveContext>
└── root: HashMap<PathKey, SelectorNode>
└── [0] → SelectorNode
└── [1] → SelectorNode
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]
#[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>,
}
#[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;
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;
}
pub trait Writable: Readable {
type WriteMetadata;
fn try_write_unchecked(&self) -> Result<WritableRef<T>>;
}
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>;
}
| 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 |