Deriving stocks
#[derive(Stock)] generates one accessor per field, so you can drill into
nested state and get a reactive handle on just that part.
This is the struct the book’s examples drill into. It is the real one (the
live demos compile against it), and the Serialize / Deserialize derives are
there only because it also crosses the bridge.
#[derive(Stock, Serialize, Deserialize)]pub struct State { count: i32, items: Vec<i32>,}let state = Stock::new(State { count: 0, items: vec![] });
let count = state.count(); // Stock<i32>let items = state.items(); // Stock<Vec<i32>>Each accessor is itself a stock. You can read it, write it, memo it, or turn it into a hail.
Accessors work on every stock kind and keep its nature: on a Stock they hand
out writable stocks, on a ReadStock read-only
ones. A step that might miss (a collection get, an enum variant) always
yields the Opt counterpart.
Collections
Section titled “Collections”Vec and HashMap stocks get get, which returns an opt
stock: the entry may not exist.
let first = state.items().get(0); // OptStock<i32>let apples = state.fruits().get("apple"); // OptStock<u32>On an enum, accessors are generated per variant. A variant is only active some of the time, so these are opt stocks too.
#[derive(Stock)]enum Shape { Circle(f32), Rect { w: f32, h: f32 },}
let radius = shape.circle(); // OptStock<f32>: None unless it is a CircleAccessor names are the variant name in snake case, so Rect becomes rect().
Writes notify only the path that changed
Section titled “Writes notify only the path that changed”This is the part worth understanding.
Derivation is path-selective. Writing items[0] notifies whatever is
watching items[0], and its ancestors: items, and the root. It does not
notify items[1].
*state.items().get(0).write() = 5;- a hail on
Item(0)→ recomputed - a hail on
Items→ recomputed (an ancestor) - a hail on
Item(1)→ untouched - a hail on
Count→ untouched
So a list of a thousand rows does not rerender because one row changed. You get that without writing any comparison logic.
Borrowing is not path-selective
Section titled “Borrowing is not path-selective”Notification is per path. Borrowing is per root, and mixing the two up is the easiest mistake to make here.
let first = state.items().get(0).read();*state.count().write() += 1; // BorrowConflict: same root, overlapping guardsThose are different paths, and the write notifies nothing the read is watching, yet the guards still collide. Two reads are fine; a write needs the other guard already dropped:
let first = *state.items().get(0).read().unwrap(); // dropped here*state.count().write() += first;Worth knowing before you reach for a memo that updates a counter while reading
other state: put that counter in its own Stock, and the question does not come
up. See when an access can fail.
See the selectivity
Section titled “See the selectivity”On the Rust side of this demo, one effect watches items[0] and another
watches items[1]. Each counts how often it actually ran.
import { usePier } from "../../setup/solid/bridge";
export default function PathsDemo() { const pier = usePier(); const [first, setFirst] = pier.hail({ Item: 0 }); // writable, path-derived const [second, setSecond] = pier.hail({ Item: 1 }); const watch0 = pier.readHail("Watch0Runs"); // effect watching items[0] const watch1 = pier.readHail("Watch1Runs"); // effect watching items[1]
return ( <div class="demo"> <p> items[0]: <b id="item0">{first()}</b> · its watcher ran{" "} <b id="w0">{watch0()}</b> times </p> <p> items[1]: <b id="item1">{second()}</b> · its watcher ran{" "} <b id="w1">{watch1()}</b> times </p> <button id="bump0" onClick={() => setFirst((first() ?? 0) + 1)}> +1 on items[0] </button> <button id="bump1" onClick={() => setSecond((second() ?? 0) + 1)}> +1 on items[1] </button> </div> );}import { useHail, useReadHail } from "../../setup/react/bridge";
export default function PathsDemo() { const [first, setFirst] = useHail({ Item: 0 }); // writable, path-derived const [second, setSecond] = useHail({ Item: 1 }); const watch0 = useReadHail("Watch0Runs"); // effect watching items[0] const watch1 = useReadHail("Watch1Runs"); // effect watching items[1]
return ( <div className="demo"> <p> items[0]: <b id="item0">{first}</b> · its watcher ran <b id="w0">{watch0}</b>{" "} times </p> <p> items[1]: <b id="item1">{second}</b> · its watcher ran <b id="w1">{watch1}</b>{" "} times </p> <button id="bump0" onClick={() => setFirst((first ?? 0) + 1)}> +1 on items[0] </button> <button id="bump1" onClick={() => setSecond((second ?? 0) + 1)}> +1 on items[1] </button> </div> );}<script setup lang="ts">import { useHail, useReadHail } from "../../setup/vue/bridge";
const first = useHail({ Item: 0 }); // writable, path-derivedconst second = useHail({ Item: 1 });const watch0 = useReadHail("Watch0Runs"); // effect watching items[0]const watch1 = useReadHail("Watch1Runs"); // effect watching items[1]</script>
<template> <div class="demo"> <p> items[0]: <b id="item0">{{ first }}</b> · its watcher ran <b id="w0">{{ watch0 }}</b> times </p> <p> items[1]: <b id="item1">{{ second }}</b> · its watcher ran <b id="w1">{{ watch1 }}</b> times </p> <button id="bump0" @click="first = (first ?? 0) + 1">+1 on items[0]</button> <button id="bump1" @click="second = (second ?? 0) + 1">+1 on items[1]</button> </div></template><script lang="ts"> import { useHail, useReadHail } from "../../setup/svelte/bridge";
const first = useHail({ Item: 0 }); // writable, path-derived const second = useHail({ Item: 1 }); const watch0 = useReadHail("Watch0Runs"); // effect watching items[0] const watch1 = useReadHail("Watch1Runs"); // effect watching items[1]</script>
<div class="demo"> <p> items[0]: <b id="item0">{$first}</b> · its watcher ran <b id="w0">{$watch0}</b> times </p> <p> items[1]: <b id="item1">{$second}</b> · its watcher ran <b id="w1">{$watch1}</b> times </p> <button id="bump0" on:click={() => ($first = ($first ?? 0) + 1)}>+1 on items[0]</button> <button id="bump1" on:click={() => ($second = ($second ?? 0) + 1)}>+1 on items[1]</button></div>Running now, in your browser
Bump items[0] and only its watcher moves. The two paths share a Vec, a
struct, and a root stock, yet still do not wake each other up.
Extend with #[stock]
Section titled “Extend with #[stock]”#[stock] attaches methods to Stock<YourType> through an extension trait.
#[derive(Stock)]struct Pair { x: u32, y: u32,}
#[stock]impl Stock<Pair> { fn sum(&self) -> u32 { *self.x().peek() + *self.y().peek() }
fn swap(&self) { let x = *self.x().peek(); let y = *self.y().peek(); self.x().set(y); self.y().set(x); }}
pair.sum();pair.swap();Use it to keep state logic next to the state instead of scattered across runners.
Pass a name if you want to control the generated trait:
#[stock(PointOps)]impl Stock<Point> { /* ... */ }Skipping a field
Section titled “Skipping a field”Mark a field or variant to leave it out of the generated accessors:
#[derive(Stock)]struct State { count: i32, #[stock(skip)] scratch: Vec<u8>,}Accessors give you the values. Memo and Effect is how you react to them.