7 releases
0.1.0 | Nov 30, 2024 |
---|---|
0.1.0-rc3 | Nov 28, 2024 |
0.1.0-rc0 | Oct 22, 2024 |
#152 in Data structures
5,842 downloads per month
Used in 27 crates
(via tachys)
560KB
13K
SLoC
Stores
Stores are a data structure for nested reactivity.
The reactive_graph
crate provides primitives for fine-grained reactivity
via signals, memos, and effects.
This crate extends that reactivity to support reactive access to nested dested, without the need to create nested signals.
Using the #[derive(Store)]
macro on a struct creates a series of getters that allow accessing each field. Individual fields
can then be read as if they were signals. Changes to parents will notify their children, but changing one sibling field will
not notify any of the others, nor will it require diffing those sibling fields (unlike earlier solutions using memoized “slices”).
This is published for use with the Leptos framework but can be used in any scenario where reactive_graph
is being used
for reactivity.
lib.rs
:
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
- work with plain Rust data types, and
- 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:
- An
Arc<RwLock<T>>
that holds the actual value - 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.
Dependencies
~2.5–4.5MB
~84K SLoC