Effects
An effect runs code when something it read changes, and cleans up after itself.
Show the code
import { onCleanup, onMount, useSignal } from "@shadow-js/core";
import { For, Show } from "@shadow-js/web";
export function Timer() {
const [on, setOn] = useSignal(false);
const [log, setLog] = useSignal(["The timer is off."]);
const write = (line: string) => setLog((lines) => [...lines, line].slice(-4));
function Ticking() {
onMount(() => {
let ticks = 0;
write("mounted: the interval starts");
const id = setInterval(() => write(`tick ${++ticks}`), 900);
onCleanup(() => {
clearInterval(id);
write("cleanup: the interval is cleared");
});
});
return null;
}
return (
<div class="stack">
<button type="button" role="switch" aria-checked={on()} onClick={() => setOn(!on())}>
{on() ? "Stop the timer" : "Start the timer"}
</button>
<Show when={on()}>
<Ticking />
</Show>
<ul aria-live="polite">
<For each={log()}>{(line) => <li>{line}</li>}</For>
</ul>
</div>
);
}
Start the timer, then stop it. onMount runs once when the component is created and the page is ready. onCleanup registers what to undo, and it runs when the component goes away. Both belong to the component that is on screen, so removing it from the page removes its timer too.
useEffect#
useEffect(fn) runs fn once at the first flush, and again whenever a signal it read changes. Which signals it depends on is not a list you write: it is whatever the last run read.
import { createRoot, flushSync, useEffect, useStore } from "@shadow-js/core";
createRoot((dispose) => {
const [name, setName] = useStore("Ada");
useEffect(() => {
console.log(`name: ${name()}`);
});
flushSync();
setName("Grace");
flushSync();
dispose();
});
name: Ada
name: GraceAn effect runs in a microtask after a write, never in the middle of your code, and never on the server. To undo something before the next run, call onCleanup inside the effect:
import { createRoot, flushSync, onCleanup, useEffect, useStore } from "@shadow-js/core";
createRoot((dispose) => {
const [name, setName] = useStore("Ada");
useEffect(() => {
const who = name();
console.log(`open ${who}`);
onCleanup(() => console.log(`close ${who}`));
});
flushSync();
setName("Grace");
flushSync();
dispose();
});
open Ada
close Ada
open Grace
close GraceAn effect created with no owner, outside a component or a createRoot, has nothing to dispose it, so development builds warn with NO_OWNER_EFFECT.
Go deeper#
Effects and lifecycle has the exact order of effects, cleanups and mounts, useRenderEffect, and the rules for effects below a Suspense boundary.