Crate reactive_stores

Source
Expand description

Stores are a primitive for creating deeply-nested reactive state, based on reactive_graph.

Reactive signals allow you to define atomic units of reactive state. However, signals are imperfect as a mechanism for tracking reactive change in structs or collections, because they do not allow you to track access to individual struct fields or individual items in a collection, rather than the struct as a whole or the collection as a whole. Reactivity for individual fields can be achieved by creating a struct of signals, but this has issues; it means that a struct is no longer a plain data structure, but requires wrappers on each field.

Stores attempt to solve this problem by allowing arbitrarily-deep access to the fields of some data structure, while still maintaining fine-grained reactivity.

The Store macro adds getters and setters for the fields of a struct. Call those getters or setters on a reactive Store or ArcStore, or to a subfield, gives you access to a reactive subfield. This value of this field can be accessed via the ordinary signal traits (Get, Set, and so on).

The Patch macro allows you to annotate a struct such that stores and fields have a .patch() method, which allows you to provide an entirely new value, but only notify fields that have changed.

Updating a field will notify its parents and children, but not its siblings.

Stores can therefore

  1. work with plain Rust data types, and
  2. provide reactive access to individual fields

§Example

use reactive_graph::{
    effect::Effect,
    traits::{Read, Write},
};
use reactive_stores::{Patch, Store};

#[derive(Debug, Store, Patch, Default)]
struct Todos {
    user: String,
    todos: Vec<Todo>,
}

#[derive(Debug, Store, Patch, Default)]
struct Todo {
    label: String,
    completed: bool,
}

let store = Store::new(Todos {
    user: "Alice".to_string(),
    todos: Vec::new(),
});

Effect::new(move |_| {
    // you can access individual store withs field a getter
    println!("todos: {:?}", &*store.todos().read());
});

// won't notify the effect that listen to `todos`
store.todos().write().push(Todo {
    label: "Test".to_string(),
    completed: false,
});

§Implementation Notes

Every struct field can be understood as an index. For example, given the following definition

#[derive(Debug, Store, Patch, Default)]
struct Name {
    first: String,
    last: String,
}

We can think of first as 0 and last as 1. This means that any deeply-nested field of a struct can be described as a path of indices. So, for example:

#[derive(Debug, Store, Patch, Default)]
struct User {
    user: Name,
}

#[derive(Debug, Store, Patch, Default)]
struct Name {
    first: String,
    last: String,
}

Here, given a User, first can be understood as [0, 0] and last is [0, 1].

This means we can implement a store as the combination of two things:

  1. An Arc<RwLock<T>> that holds the actual value
  2. A map from field paths to reactive “triggers,” which are signals that have no value but track reactivity

Accessing a field via its getters returns an iterator-like data structure that describes how to get to that subfield. Calling .read() returns a guard that dereferences to the value of that field in the signal inner Arc<RwLock<_>>, and tracks the trigger that corresponds with its path; calling .write() returns a writeable guard, and notifies that same trigger.

Structs§

Traits§

  • Extends optional store fields, with the ability to unwrap or map over them.
  • Allows updating a store or field in place with a new value.
  • Allows patching a store field with some new value.
  • Describes a type that can be accessed as a reactive store field.
  • Provides unkeyed reactive access to the fields of some collection.

Derive Macros§