Overview

effect(fn) runs fn immediately and re-runs it whenever any atom read inside fn changes. If fn returns a function, that cleanup runs before the next execution and when the effect is disposed.

The return value of effect() itself is a dispose function — calling it stops the effect permanently. Effects created in a component outer function are disposed automatically on unmount.

Re-runs are scheduled (batched via microtask), not synchronous — do not assume the effect has re-run immediately after atom.set().

effect vs on / watch

API
Deps
First run
effect(fn) Auto-tracked reads Runs immediately on create; then on dep change.
on(fn, deps) Explicit array Does not run on create — only when a listed dep notifies.

Reactive side effects

effect() auto-tracks every atom read inside it and re-runs when any of them change. The first log line appears on create.

import { html, atom, effect, renderApp } from 'mates';

const App = () => {
const count = atom(0);
const log = atom([]);

effect(() => {
log.set(prev => [...prev.slice(-4), `count → ${count()}`]);
});

return () => html`
<div class="m-col m-gap">
<h1>${count()}</h1>
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button class="btn-danger" @click=${() => count.set(n => n - 1)}>−</button>
<button @click=${() => count.set(0)}>Reset</button>
<button class="btn-primary" @click=${() => count.set(n => n + 1)}>+</button>
</div>
<div class="card card-body">
<span class="overline">Effect log</span>
${log().map(e => html`<div class="mono">${e}</div>`)}
</div>
</div>
`;
};

renderApp(App, document.getElementById('app'));

Cleanup before re-run

Return a cleanup function — it runs before the next execution and on dispose/unmount. Re-runs are microtask-batched after set().

import { html, atom, effect, renderApp } from 'mates';

const App = () => {
const count = atom(0);
const cleanups = atom(0);

effect(() => {
const n = count();
document.title = `count: ${n}`;
return () => {
cleanups.set(c => c + 1);
document.title = 'demo';
};
});

return () => html`
<div class="m-col m-gap">
<h1>${count()}</h1>
<p class="muted m-t-5">Cleanups run: ${cleanups()}</p>
<button class="btn-primary" @click=${() => count.set(n => n + 1)}>+1</button>
</div>
`;
};

renderApp(App, document.getElementById('app'));