From b9bf3ccc01e3ee5092ffc078947764173306d467 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 5 Aug 2025 15:59:07 -0500 Subject: [PATCH] Improve stores documentation --- .../0.7/src/essentials/basics/collections.md | 79 +++++++++++++++---- 1 file changed, 63 insertions(+), 16 deletions(-) diff --git a/docs-src/0.7/src/essentials/basics/collections.md b/docs-src/0.7/src/essentials/basics/collections.md index f164000d9..36c3ff5b2 100644 --- a/docs-src/0.7/src/essentials/basics/collections.md +++ b/docs-src/0.7/src/essentials/basics/collections.md @@ -20,9 +20,9 @@ To make working with structs and collections easier, Dioxus provides the **Store ## Reactive Stores -In Dioxus, reactive stores are types that isolate reactivity to just a single field or entry in a collection. Stores allow us to "zoom in" on a smaller portion of our data, ignoring all other reads and writes. +In Dioxus, reactive stores are types that isolate reactivity to just a path in a data structure. Stores allow us to "zoom in" on a smaller portion of our data, ignoring all other reads and writes. -The simplest stores are structs that derive the `Store` trait: +The simplest stores are structs that derive a `Store` trait: ```rust #[derive(Store)] @@ -41,7 +41,7 @@ let header = use_store(|| HeaderState { }); ``` -The `Store` drive macro generates additional methods on `HeaderState` that allow us to "zoom in" to fields of the struct. We access the fields by calling the field name like a method: +The `Store` drive macro generates additional methods on `Store` that allow us to "zoom in" to fields of the struct. We access the fields by calling the field name like a method: ```rust fn app() -> Element { @@ -74,13 +74,22 @@ Notice how the default `Store` we get from `use_store` has an elided default gen let title: Store = use_store(|| HeaderState::new()); ``` -Because the lens is "unnamable," we can't easily add it to structs or pass it as a function argument. In these cases, we can use the `boxed` and `boxed_mut` methods to convert the lens into a ReadSignal or WriteSignal at the cost of an allocation: +Because the lens is "unnamable", we need to accept the lens as a generic in any functions that work with stores. If we only need to read the store, we can require the lens implements the `Readable` trait. If we need to write to the store, we can require the lens implements the `Writable` trait. ```rust -let title: ReadSignal = header.title().boxed(); +// This function works with any lens that can read the header state like ReadStore or Store +fn get_title(state: Store>) -> String { + state.title().cloned() +} + +// This function works with any lens that can write to the header state like Store or Store +fn clear_title(state: Store>) { + state.title().take(); +} ``` -On the boundary of components, this is done automatically by "decaying" lenses into ReadSignals: +On the component boundary, Stores are automatically boxed and converted to `ReadSignal` or `ReadStore` as needed so you don't need to worry about the lens type: + ```rust fn app() -> Element { @@ -92,6 +101,8 @@ fn app() -> Element { rsx! { // the lens returned by `.title()` decays into a `ReadSignal` automatically! Title { title: header.title() } + // the lens returned by `.subtitle()` decays into a `ReadStore` automatically! + Subtitle { subtitle: header.subtitle() } } } @@ -99,6 +110,11 @@ fn app() -> Element { fn Title(title: ReadSignal) -> Element { // .. } + +#[component] +fn Subtitle(subtitle: ReadStore) -> Element { + // .. +} ``` ### Stores are Readable and Writable @@ -213,7 +229,7 @@ let len = match header.subtitle().transpose() { }; ``` -Alternatively, we can use `.ref()` the lens to gain access to the underlying value, but we lose the ability to reactively "zoom in" further: +Alternatively, we can use `.as_ref()` the lens to gain access to the underlying value, but we lose the ability to reactively "zoom in" further: ```rust let len = match header.subtitle().as_ref() { @@ -222,7 +238,7 @@ let len = match header.subtitle().as_ref() { }; ``` -You can usually choose either approach - just know that using `.ref()` calls `.read()` internally, and the "reactivity zoom" might not be perfectly precise. +You can usually choose either approach - just know that using `.as_ref()` calls `.read()` internally, and the "reactivity zoom" might not be perfectly precise. ## Reactive Collections @@ -284,7 +300,7 @@ fn app() -> Element { let mut users = use_store(|| HashMap::::new()); rsx! { - for (id, user) in users.read() { + for (id, user) in users.iter() { ListItem { key: "{id}", user } } } @@ -301,24 +317,55 @@ fn ListItem(user: ReadSignal) -> Element { The `Store>` type is a special type that implements reactivity on a per-entry basis. When we insert or remove values from the `users` store, only *one* re-render is queued. If we edit an individual entry in the HashMap, only a single `ListItem` will re-render. -Alternatively, we could pass the entire `Store` to the ListItem, along with the `UserId` key, allowing us to further lens into specific fields of our UserData entries: +Alternatively, we could derive `Store` on our `UserData` type, and accept `ReadStore` allowing us to further lens into specific fields of our UserData entry: ```rust +#[derive(Store)] +struct UserData { + name: String, + email: String, +} + fn app() -> Element { - // switch to using `use_store` - let mut users = use_store(|| HashMap::::new()); + let users = use_store(|| HashMap::::new()); rsx! { - for (id, user) in users.read() { - ListItem { key: "{id}", users, id } + for (id, user) in users.iter() { + ListItem { key: "{id}", user } } } } #[component] -fn ListItem(users: Store>, id: UserId) -> Element { +fn ListItem(user: ReadStore) -> Element { rsx! { - li { "{users.get(id)).read()}" } + li { "{user.name()}" } + } +} +``` + +## Extending Stores with Methods + +You can extend your store types with methods with the `#[store]` attribute macro. Methods inside the macro are converted into an extension trait that is automatically implemented for `Store`. The macro will automatically add bounds to the `Lens` generic based on the self parameter of the method. If the method takes `&self`, the `Lens` will be bound by `Readable`. If the method takes `&mut self`, the `Lens` will be bound by `Writable`. + +```rust +type MappedUserDataStore = Store &String, fn(&mut UserData) -> &mut String>>; + +#[store] +impl Store { + // This will automatically require `Readable` on the lens since it takes `&self` + fn user_email(&self) -> String { + self.email().cloned() + } + + // This will automatically require `Writable` on the lens since it takes `&mut self` + fn clear_name(&mut self) { + self.name().take(); + } + + // This method does not require any bounds on the lens since it takes `self` + fn into_parts(self) -> (MappedUserDataStore, MappedUserDataStore) where Self: Copy { + (self.email(), self.name()) } } ```