Memo and Effect
You never register a dependency by hand. Any read() inside a memo or effect
records one, and that is what decides what re-runs later.
A Memo<T> is a cached derived value.
let count = Stock::new(1i32);let doubled = Memo::new(move || *count.read() * 2);
println!("{}", *doubled.read()); // 2Or straight from a stock:
let doubled = count.memo(|c| *c * 2);let total = state.items().memo(|v| v.iter().sum::<i32>());A memo recomputes when an input changes. It propagates only when the result
actually differs, which is why the result type needs PartialEq.
That second part matters more than it sounds:
let parity = count.memo(|c| *c % 2);let label = parity.memo(|p| if *p == 0 { "even" } else { "odd" });Changing count from 1 to 3 recomputes parity, gets 1 again, and stops.
label never runs. Chains stay cheap without you checking anything.
See it stop
Section titled “See it stop”That last claim is the kind you normally have to take on faith: nothing happening looks exactly like nothing happening.
So this demo counts it. The label memo increments a counter every time its
body actually runs.
import { usePier } from "../../setup/solid/bridge";
export default function MemoDemo() { const pier = usePier(); const count = pier.readHail("Count"); const parity = pier.readHail("Parity"); // count % 2 const label = pier.readHail("Label"); // memo over parity const runs = pier.readHail("LabelRuns"); // times the label memo ran
return ( <div class="demo"> <p> count: <b id="count">{count()}</b> · parity: <b id="parity">{parity()}</b> · label: <b id="label">{label()}</b> </p> <p> label memo has run <b id="runs">{runs()}</b> times </p> <button id="bump-1" onClick={() => pier.tell({ Bump: 1 })}> +1 </button> <button id="bump-2" onClick={() => pier.tell({ Bump: 2 })}> +2 </button> </div> );}import { useReadHail, useTell } from "../../setup/react/bridge";
export default function MemoDemo() { const count = useReadHail("Count"); const parity = useReadHail("Parity"); // count % 2 const label = useReadHail("Label"); // memo over parity const runs = useReadHail("LabelRuns"); // times the label memo ran const tell = useTell();
return ( <div className="demo"> <p> count: <b id="count">{count}</b> · parity: <b id="parity">{parity}</b> · label:{" "} <b id="label">{label}</b> </p> <p> label memo has run <b id="runs">{runs}</b> times </p> <button id="bump-1" onClick={() => tell({ Bump: 1 })}> +1 </button> <button id="bump-2" onClick={() => tell({ Bump: 2 })}> +2 </button> </div> );}<script setup lang="ts">import { useReadHail, useTell } from "../../setup/vue/bridge";
const count = useReadHail("Count");const parity = useReadHail("Parity"); // count % 2const label = useReadHail("Label"); // memo over parityconst runs = useReadHail("LabelRuns"); // times the label memo ranconst tell = useTell();</script>
<template> <div class="demo"> <p> count: <b id="count">{{ count }}</b> · parity: <b id="parity">{{ parity }}</b> · label: <b id="label">{{ label }}</b> </p> <p> label memo has run <b id="runs">{{ runs }}</b> times </p> <button id="bump-1" @click="tell({ Bump: 1 })">+1</button> <button id="bump-2" @click="tell({ Bump: 2 })">+2</button> </div></template><script lang="ts"> import { useReadHail, useTell } from "../../setup/svelte/bridge";
const count = useReadHail("Count"); const parity = useReadHail("Parity"); // count % 2 const label = useReadHail("Label"); // memo over parity const runs = useReadHail("LabelRuns"); // times the label memo ran const tell = useTell();</script>
<div class="demo"> <p> count: <b id="count">{$count}</b> · parity: <b id="parity">{$parity}</b> · label: <b id="label">{$label}</b> </p> <p> label memo has run <b id="runs">{$runs}</b> times </p> <button id="bump-1" on:click={() => tell({ Bump: 1 })}>+1</button> <button id="bump-2" on:click={() => tell({ Bump: 2 })}>+2</button></div>Running now, in your browser
Press +2 a few times: count climbs, parity and label sit still, and the
run counter does not move. Press +1 and it ticks up by one.
That is the whole point of the PartialEq bound. Note the counter is a
hail like any other, so it arrives in the same dispatch as the value it
explains.
A memo is lazy. It computes on first read, then only when something it read has changed.
Effect
Section titled “Effect”An Effect runs for its side effect, and re-runs when its dependencies change.
let logger = Effect::new(move || { log(&format!("count is {}", *count.read()));});Use it for things outside the reactive graph: logging, storage, calling out to a browser API.
Do not use one to compute a value. That is what a memo is for, and a memo will be both cheaper and easier to follow.
Reading without subscribing
Section titled “Reading without subscribing”read() subscribes. peek() does not.
let ratio = Memo::new(move || { let n = *numerator.read(); // re-runs when this changes let d = *denominator.peek(); // does not n / d});Use peek() when you need a value but do not want it to be a trigger. A common
case is a helper method that should not drag extra dependencies into whichever
memo happens to call it.
Grouping writes
Section titled “Grouping writes”Every write propagates when its guard drops. To make several writes settle
together, wrap them in batch:
batch(|| { state.count().set(0); state.items().write().clear();});Dependents run once at the end instead of after each write.
You rarely need this from a tell: the bridge already batches everything it dispatches to JS in one pass. Reach for it when a single logical change touches several stocks and an effect in between would see a half-updated state.
Both of these are synchronous. Async work covers the rest.