Update complex data iteration with memoized slices example (#246)

Update to use `ForEnumerate` rather than `For`, to support reordering of
data values as well.
This commit is contained in:
Francis Nimick
2025-12-18 14:53:23 -05:00
committed by GitHub
parent 700357e6a1
commit 8b15f5b886

View File

@@ -235,12 +235,42 @@ Leptos provides a primitive called a [`Memo`](https://docs.rs/leptos/latest/lept
which creates a derived computation that only triggers a reactive update when its value
has changed.
This allows you to create reactive values for subfields of a larger data structure,
without needing to wrap the fields of that structure in signals.
This allows you to create reactive values for subfields of a larger data structure, without needing
to wrap the fields of that structure in signals. In combination with
[`<ForEnumerate/>`](https://docs.rs/leptos/latest/leptos/control_flow/fn.ForEnumerate.html), this
will allow us to rerender only changed data values.
Most of the application can remain the same as the initial (broken) version, but the `<For/>`
will be updated to this:
```rust
<ForEnumerate
each=move || data.get()
key=|state| state.key.clone()
children=move |index, _| {
let value = Memo::new(move |_| {
data.with(|data| data.get(index.get()).map(|d| d.value).unwrap_or(0))
});
view! {
<p>{value}</p>
}
}
/>
```
Youll notice a few differences here:
- we use `ForEnumerate` rather than `For`, so we have access to an `index` signal
- we use the `children` prop explicitly, to make it easier to run some non-`view` code
- we define a `value` memo and use that in the view. This `value` field doesnt actually
use the `child` being passed into each row. Instead, it uses the index and reaches back
into the original `data` to get the value.
Now every time `data` changes, each memo will be recalculated. If its value has changed,
it will update its text node, without rerendering the whole row.
**Note**: It is not safe to use `For` for this with an enumerated iterator, as in an earlier version of this example:
```rust
<For
each=move || data.get().into_iter().enumerate()
@@ -256,16 +286,9 @@ will be updated to this:
/>
```
Youll notice a few differences here:
- we convert the `data` signal into an enumerated iterator
- we use the `children` prop explicitly, to make it easier to run some non-`view` code
- we define a `value` memo and use that in the view. This `value` field doesnt actually
use the `child` being passed into each row. Instead, it uses the index and reaches back
into the original `data` to get the value.
Every time `data` changes, now, each memo will be recalculated. If its value has changed,
it will update its text node, without rerendering the whole row.
In this case, changes to values in `data` will be reacted to, but changes to ordering will not, as
the Memo will always use the `index` it was initially created with. This will result in duplicate
entries in the rendered output if any items are moved.
### Pros
@@ -274,11 +297,11 @@ wrap the data in signals.
### Cons
Its a bit more complex to set up this memo-per-row inside the `<For/>` loop rather than
using nested signals. For example, youll notice that we have to guard against the possibility
that the `data[index]` would panic by using `data.get(index)`, because this memo may be
triggered to re-run once just after the row is removed. (This is because the memo for each row
and the whole `<For/>` both depend on the same `data` signal, and the order of execution for
Its a bit more complex to set up this memo-per-row inside the `<ForEnumerate/>` loop rather than
using nested signals. For example, youll notice that we have to guard against the possibility that
the `data[index.get()]` would panic by using `data.get(index.get())`, because this memo may be
triggered to re-run once just after the row is removed. (This is because the memo for each row and
the whole `<ForEnumerate/>` both depend on the same `data` signal, and the order of execution for
multiple reactive values that depend on the same signal isnt guaranteed.)
Note also that while memos memoize their reactive changes, the same
@@ -299,6 +322,7 @@ Stores are built on top of the `Store` derive macro, which creates a getter for
We can adapt the data types we used in the examples above.
The top level of a store always needs to be a struct, so well create a `Data` wrapper with a single `rows` field.
```rust
#[derive(Store, Debug, Clone)]
pub struct Data {
@@ -312,9 +336,11 @@ struct DatabaseEntry {
value: i32,
}
```
Adding `#[store(key)]` to the `rows` field allows us to have keyed access to the fields of the store, which will be useful in the `<For/>` component below. We can simply use `key`, the same key that well use in `<For/>`.
The `<For/>` component is pretty straightforward:
```rust
<For
each=move || data.rows()
@@ -325,6 +351,7 @@ The `<For/>` component is pretty straightforward:
}
/>
```
Because `rows` is a keyed field, it implements `IntoIterator`, and we can simply use `move || data.rows()` as the `each` prop. This will react to any changes to the `rows` list, just as `move || data.get()` did in our nested-signal version.
The `key` field calls `.read()` to get access to the current value of the row, then clones and returns the `key` field.
@@ -332,6 +359,7 @@ The `key` field calls `.read()` to get access to the current value of the row, t
In `children` prop, calling `child.value()` gives us reactive access to the `value` field for the row with this key. If rows are reordered, added, or removed, the keyed store field will keep in sync so that this `value` is always associated with the correct key.
In the update button handler, well iterate over the entries in `rows`, updating each one:
```rust
for row in data.rows().iter_unkeyed() {
*row.value().write() *= 2;
@@ -348,10 +376,10 @@ Personally, I think the stores version is the nicest one here. And no surprise,
On the other hand, its the newest API. As of writing this sentence (December 2024), stores have only been released for a few weeks; I am sure that there are still some bugs or edge cases to be figured out.
### Full Example
Heres the complete store example. You can find another, more complete example [here](https://github.com/leptos-rs/leptos/blob/main/examples/stores/src/lib.rs), and more discussion in the book [here](../15_global_state.md).
```
use reactive_stores::Store;