---
title: "Component Lifecycle and Listeners"
description: "Lifecycle and listeners: setup runs once and untracked, onMount attaches children first and returns the cleanup, plus onUnmount and listen()."
source: https://lemonadejs.com/docs/lifecycle/
---

# Lifecycle and listeners

A component has four lifecycle tools, `listen()` for listeners, and the
setup body itself:

```javascript
const C = (props, { onMount, onUnmount, listen, unmount }) => {
    // setup: runs ONCE, before the DOM exists
    onMount((el) => {
        const id = setInterval(tick, 1000);   // el = the first root node, attached
        return () => clearInterval(id);       // the return value IS the cleanup
    });
    listen(document, 'keydown', onKey);       // removed on unmount, automatically
    onUnmount(() => save());                  // runs on destroy
    // unmount(): the instance destroys itself (toasts, dismissable banners)
    return html`<div></div>`;
};
```

That is the complete surface. There is no update hook, because nothing
re-renders: the setup body never runs again, and updates happen per binding.

`onMount` returns its cleanup, `listen` is removed automatically: unmount the clock and both go away.

<!--example-->

```js
import { html } from 'lemonadejs';

const Clock = (props, { state, onMount, listen }) => {
    const now = state(new Date().toLocaleTimeString());
    const keys = state(0);
    onMount(() => {
        const id = setInterval(() => (now.value = new Date().toLocaleTimeString()), 1000);
        return () => clearInterval(id);              // the cleanup, run on unmount
    });
    listen(document, 'keydown', () => keys.value++); // removed on unmount, automatically
    return html`<p>${now} · keys pressed on this page: ${keys}</p>`;
};

const App = (props, { state }) => {
    const shown = state(true);
    return html`<div>
        ${() => shown.value && html`<${Clock} />`}
        <button onclick="${() => (shown.value = !shown.value)}">${() => (shown.value ? 'unmount the clock' : 'mount the clock')}</button>
    </div>`;
};
```

## Background

Two management responsibilities recur in component lifecycle:

- **Setup vs updates.** Code that runs on mount often requires a
  corresponding cleanup on unmount; code that runs whenever a value changes
  belongs in a subscription or computed value. Conflating them in one
  mechanism requires the author to disambiguate at call time.
- **DOM listeners outside the component's own elements.**
  `document.addEventListener` in setup requires a matching
  `removeEventListener` on destroy, with the *same function reference*; a
  listener armed mid-gesture (mousemove during a drag) needs removal on
  mouseup *and* on an unmount that happens mid-drag. A forgotten removal
  produces no error, just a listener calling into a dead component.

## How LemonadeJS solves it

**Setup runs once, and untracked.** The component function is invoked a
single time, with dependency tracking suspended. This is deliberate: if
setup reads ran tracked, they would subscribe the branch binding that
happens to be materializing the component, and an unrelated state change
would rebuild the whole instance, re-introducing a re-render model.
The corollary is that a state read in the setup body is a one-time snapshot
(dev builds warn `LJS-202` when that looks like a mistake); live
derivations belong in [computed()](/docs/computed/).

**`onMount(el)` runs when the DOM is actually attached**, so `document.contains(el)`
is already true, children before parents, so a parent can measure composed
children. One callback, two jobs by position: the body is the mount work,
the *returned function* is the cleanup. Non-function returns are ignored,
so single-expression callbacks stay legal:

```javascript
onMount(() => opens.value++);                       // no cleanup, fine
onMount(() => open.subscribe((v) => v && place())); // subscribe returns its
                                                    // unsubscribe → auto-cleanup
```

That second line is a real idiom from the modal block: `subscribe()` returns
the unsubscribe function, which `onMount` registers as the cleanup, wiring
and unwiring in one statement.

**`onUnmount(cb)` and every registered cleanup run on destroy**, children
first, whether the destroy came from `handle.unmount()`, a branch replacing
the content, or a removed custom element. What "complete disposal" means,
and the heap-snapshot proof, is the [Destroy](/docs/destroy/) chapter.

**`unmount()` is self-destruction.** The tool unmounts *this* instance:
children, bindings, cleanups, DOM: the full destroy path, idempotent. The
instance is flagged dead, and the reconciler never resurrects a dead
instance: if the owner re-renders the same position with the same data, a
fresh instance is built instead (the test suite pins this). Self-removal is
for presentation (a toast dismissing itself); *permanent* removal belongs
in the owner's data.

**`listen()` owns every listener the template cannot.** Listeners on the
component's *own* elements stay in the template, as in
`onclick="${...}"`, and die with their nodes. Everything else
(`document`, `window`, another element) goes through the tool, and the
doctrine is absolute: **raw `addEventListener` never appears in
component code.**

```javascript
const C = (p, { listen }) => {
    listen(document, 'mousemove', onMove);            // removed on unmount
    const off = listen(window, 'resize', onResize);   // off() removes earlier
    // off() is idempotent and SELF-PRUNING: calling it removes the
    // listener and withdraws its own unmount registration
};
```

`listen` works anywhere: setup, `onMount`, or **mid-gesture inside an
event handler**: arm `document` mousemove/mouseup on mousedown, call
`off()` on mouseup, and a mid-drag unmount still cleans up. A forgotten
`removeEventListener` is not writable, because there is nothing to
forget; removal is owned by the disposal that already owns everything
else. The engine suite pins the sharp edges: `off()` after unmount is
harmless, repeated gestures re-arm cleanly, an unmount mid-drag kills
the document listener, and multiple armed listeners all die at unmount
(the self-pruning regression test exists because the iteration bug it
guards was real).

**Branches detach; they do not destroy.** `${() => show.value && html`...`}`
mounts its component when it first appears, exactly once. Hiding detaches
the DOM (one cached generation per slot); showing again reattaches the
*same* DOM without remounting, so `onMount` and refs do not re-fire. Work
that must happen per appearance is driven by the state that controls the
branch, the modal idiom above.

## In practice

**The modal's gesture pattern: armed mid-event, one release slot.**
Drag and resize each attach `mousemove`/`mouseup` to `document`, armed
inside the mousedown handler. The shipped source, on `listen()`:

```javascript
let releaseInteraction = null;
onUnmount(() => releaseInteraction?.());     // covers an unmount MID-DRAG

const track = (move, done) => {
    releaseInteraction?.();                  // one gesture in flight, ever
    const offs = [
        listen(document, 'mousemove', move),
        listen(document, 'mouseup', () => releaseInteraction?.()),
    ];
    releaseInteraction = () => {
        offs.forEach((off) => off());        // off(): fire-once, self-pruning
        releaseInteraction = null;
        done?.();
    };
};

listen(window, 'resize', () => refreshDock());   // component-lifetime listener
```

Two layers of the same guarantee: `off()` releases per gesture, and any
listener still armed when the modal dies, mid-drag included, is
removed by disposal. The destroy-stress suite unmounts mid-drag and
asserts the document's listener count returns to baseline. The pattern
is gated, not stylistic.

**The speeddial's grace timer.** Hovering away closes the fan after 150ms;
re-entering cancels the timer, and so does unmount, because a pending
timeout that fires after death is a classic leak-adjacent bug:

```javascript
let grace = null;
const clearGrace = () => {
    if (grace !== null) { clearTimeout(grace); grace = null; }
};
onUnmount(clearGrace);                       // never a timer outliving the fan

const onLeave = () => {
    clearGrace();
    if (fanned.value) grace = setTimeout(() => { grace = null; doClose(); }, 150);
};
```

One timer in flight, three exits (re-enter, fire, unmount), one cleanup.

## Reference

```javascript
const C = (props, { onMount, onUnmount, listen, unmount }) => { ... };

onMount((el) => cleanup?)   // el: first root node, already attached;
                            // children mount before parents; a returned
                            // FUNCTION runs on destroy (anything else ignored)
onUnmount(cb)               // runs on destroy, children first
listen(target, type, fn, options?)   // any EventTarget; removed on unmount
                            // → off(): removes earlier; idempotent,
                            //   self-pruning; works mid-gesture
unmount()                   // self-destroy: full disposal, idempotent;
                            // dead instances are never resurrected

const handle = mount(C, root);   // { el, unmount() }
handle.unmount();                // owner-driven destroy
```

Rules worth memorizing: setup runs once and untracked, so derive with
`computed()`, never in the setup body; the `onMount` return value is the
cleanup; listeners on your own elements stay in the template, everything
else goes through `listen()`; raw `addEventListener` never appears in
component code; branch show/hide reattaches without remounting, so re-arm
per-open work from the controlling state. For what destruction guarantees
(and the receipts), read [Destroy](/docs/destroy/).