When a write reaches effects and the DOM
Signal writes and store writes follow the same scheduler rule (ADR-007).
Signal writes and store writes follow the same scheduler rule (ADR-007). A write marks its readers stale at once; reads always see the new value. Effects, including the render effects that update the DOM, run in a microtask unless something flushes earlier:
| Where the write happens | When effects run |
|---|---|
| Plain code (a timer, a promise callback, module code) | In a microtask. All writes made in the same tick share one run. |
Inside batch(fn) |
Synchronously, once, when the outermost batch ends. |
| Inside an event handler bound by JSX | Synchronously, once, after all handlers for that event have run. |
flushSync() or flushSync(fn) |
Synchronously, now. |
src/timing.tsts
import { batch, createRoot, flushSync, useEffect, useStore } from "@shadow-js/core";
const [count, setCount] = useStore(0);
const [form, setForm] = useStore({ name: "Ada" });
const log: string[] = [];
createRoot(() => {
useEffect(() => {
log.push(`effect: ${count()} ${form.name}`);
});
});
flushSync();
console.log(log.length); // 1: the first run
setCount(1); // a signal write
setForm("name", "Grace"); // a store write
console.log(log.length); // 1 again: nothing has run yet
await Promise.resolve();
console.log(log.length); // 2: one run covered both writes
batch(() => {
setCount(2);
setForm("name", "Hopper");
});
console.log(log.length); // 3: the batch flushed on exit
setCount(3);
flushSync();
console.log(log.length); // 4
console.log(log.join("; "));
1
1
2
3
4
effect: 0 Ada; effect: 1 Grace; effect: 2 Hopper; effect: 3 HopperConsequences:
-
Reading state right after writing it always gives the new value. Reading the DOM right after writing does not, unless you are in an event handler, a
batchor afterflushSync(). -
A write inside a
useMemocomputation throwsWRITE_IN_MEMOin development; see Derived values.