[]previous and next page

Effects

An effect runs code when something it read changes, and cleans up after itself.

For agentsUpdated
Show the code
Timer.tsxtsx
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.

src/effect.tsts
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();
});
Expected output
name: Ada
name: Grace

An 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:

src/effect-cleanup.tsts
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();
});
Expected output
open Ada
close Ada
open Grace
close Grace

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