REFERENCE / CORE API

onCleanup()

Registers teardown for the current scope. It runs before every re-run of the owning effect, and once more when the scope is disposed for good — children before parents.

Signature

TS
onCleanup(fn: () => void): void

Parameters

PARAMETERTYPEDESCRIPTION
fn () => void The teardown. Called synchronously at the moments above; it may be registered any number of times in a scope.

In a component

A component function is a scope. Anything it starts — timers, observers, subscriptions, third-party widgets — is released here when the component is removed by a conditional, a list, or an explicit dispose.

JS
function Clock() {
  const now = signal(new Date());
  const id = setInterval(() => (now.value = new Date()), 1000);
  onCleanup(() => clearInterval(id));   // when the Clock's scope dies, so does the interval
  return html`<time>${() => now.value.toLocaleTimeString()}</time>`;
}

In an effect

Inside an effect, cleanup also runs before each re-run, so the previous run's resources never leak into the next.

JS
effect(() => {
  const el = target.value;                         // which element to watch
  const ro = new ResizeObserver(report);
  ro.observe(el);
  onCleanup(() => ro.disconnect());                // before the effect re-runs for a new target, and on dispose
});

Where it is legal

Anywhere a scope is active: a component function, an effect, a root(), a list row. mount() wraps its component in an app-lifetime root, so a top-level component may use it too. Outside any scope there is nothing to attach to.

Try it

src/islands/cleanup.ts
import { signal, effect, onCleanup, html, mount } from '@amojs.dev/core';

const ms = signal(1000);
const pulses = signal(0);
const log = signal<string[]>([]);
const note = (line: string) => (log.value = [...log.peek(), line]);

// the effect re-runs when `ms` changes; onCleanup runs BEFORE each re-run, so the old timer never leaks
effect(() => {
  const period = ms.value; // captured: by the time cleanup runs, ms already holds the NEW value
  const id = setInterval(() => pulses.value++, period);
  note(`setup: every ${period} ms`);
  onCleanup(() => {
    clearInterval(id);
    note(`cleanup: the ${period} ms timer`);
  });
});

const Demo = () => html`<div class="demo">
  <p>pulses: <b>${pulses}</b></p>
  <div class="row">
    <button class="counter" onclick=${() => (ms.value = 250)}>every 250 ms</button>
    <button class="counter" onclick=${() => (ms.value = 1000)}>every 1000 ms</button>
  </div>
  <ul class="log">${() => log.value.map((line) => html`<li>${line}</li>`)}</ul>
</div>`;

const host = document.getElementById('demo');
if (host) mount(Demo, host);
live preview● running

See also