Skip to content

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()); // 2

Or 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.

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.

Memo.tsx
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>
);
}

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.

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.

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.

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.