Side effects — effect

An effect runs a function and re-runs it whenever a reactive value it reads changes. It's the third member of the reactive trio: createState holds a value, computed derives one, and effect does something when values change.

You rarely need effect for what's on screen — a binding (${() => …}) already updates the DOM for you. Reach for effect to do something outside the view: persist to localStorage, set document.title, log, or drive a non-Zoijs widget.
import { createState, effect } from "./src/index.js";

const theme = createState("light");

// Runs now, and again every time `theme` changes:
effect(() => localStorage.setItem("theme", theme.get()));

theme.set("dark"); // → writes "dark" to localStorage

It tracks dependencies automatically#

Whatever the function reads becomes a dependency — there is no dependency array (and none of the bugs that come with one). Read a.get() and b.get() and the effect re-runs when either changes; stop reading one and it stops depending on it. It runs once immediately, then again (batched on a microtask) on each change.

Cleanup: return a function#

If your effect sets something up that needs tearing down, return a cleanup function. It runs before the next run and once more on dispose — the same convention as a ref.

effect(() => {
  const id = setInterval(() => poll(query.get()), 1000);
  return () => clearInterval(id); // before each re-run, and on dispose
});

This is the difference from onCleanup: a returned cleanup is per-run (it fires every time the effect re-runs), while onCleanup fires once, when the component unmounts. Use the return value for things you set up inside the effect.

It cleans up with its owner#

An effect created inside a component or list item is disposed automatically when that component unmounts or the item is removed — like everything else in Zoijs. You usually don't manage its lifetime.

Created at module top level (outside any component), it lives until you stop it yourself with the returned handle:

const handle = effect(() => (document.title = `${unread.get()} unread`));
handle.dispose(); // stop it

effect vs. a binding#

You want…Use
reactive text or attributes on screena binding — ${() => value.get()}
a side effect off screen (storage, title, logging, a widget)effect(() => …)

If you find yourself poking the DOM by hand inside an effect, that's usually a sign a binding belongs there instead.

effect is not a lifecycle system#

There is no onMounted / onUpdated / beforeUnmount family. Zoijs has just two tools here: effect (react to change) and onCleanup (tear down). Together they cover the space without a lifecycle API.


Next: Error boundaries with boundary().