# Scopes

Every `effect`, and `onCleanup` belongs to a **scope**. When the scope disposes, everything inside it tears down. Most JSX boundaries create a scope for you — knowing which ones, and where that scope is missing, is the whole contract.

## The table

| Boundary                                            | Auto-scoped                   | Disposed when                                             |
| --------------------------------------------------- | ----------------------------- | --------------------------------------------------------- |
| Function component body — `(props) => <el/>`        | yes                           | the element is removed                                    |
| Intrinsic or class element — `<div/>`, `<MyClass/>` | yes                           | the element is removed                                    |
| Fragment — `<>...</>`                               | yes (via the JSX transform)   | the fragment's disposables run                            |
| Reactive child slot — `{() => signal()}`            | yes                           | the slot updates or its parent unmounts                   |
| `<For>` render callback, per item                   | yes                           | the item's key leaves `each`, or the list unmounts        |
| Custom element `connectedCallback`                  | **no** — opt in with `render` | you call the returned `unmount` in `disconnectedCallback` |
| App root (`document.getElementById("app")`)         | **no** — opt in with `render` | you call the returned `unmount`                           |

If you write `onCleanup(...)` or `effect(...)` inside any of the first five rows, the library owns its lifetime. At the boundaries below, you own it.

## `render` — mount with a scoped lifetime

`render(target, setup)` runs `setup` inside a detached `effectScope`, appends its returned node to `target`, and hands back a single `unmount` thunk. Calling `unmount` removes the node from the DOM, disposes its `Symbol.dispose` hook, and tears down every `effect` / `onCleanup` registered inside `setup`.

### Mounting an app

Typical entry point — mount a root component into the page container and keep a reference to the `unmount` thunk (e.g. for hot-module replacement):

```tsx
import { render } from "elements-kit/render";
import { App } from "./App";

const unmount = render(
  document.getElementById("app")!,
  () => <App /> as Element,
);
```

### Inside a custom element

Use the same helper in `connectedCallback`; stash the returned `unmount` on the element and call it from `disconnectedCallback`.

```tsx
import { onCleanup } from "elements-kit/signals";
import { render } from "elements-kit/render";

class Clock extends HTMLElement {
  #unmount?: () => void;

  #template = () => {
    const el = document.createElement("time");
    const id = setInterval(() => (el.textContent = String(Date.now())), 1000);
    onCleanup(() => clearInterval(id));
    return el;
  };

  connectedCallback() {
    this.#unmount = render(this, this.#template);
  }

  disconnectedCallback() {
    this.#unmount?.();
    this.#unmount = undefined;
  }
}
```

> **Effects must live inside the render callback**
>
> Put every `effect` and `onCleanup` **inside** the `render` callback. Anything created outside — in the constructor, a field initializer, or `attributeChangedCallback` — has no owning scope and will not stop when the element disconnects. The same rule applies to effects started from a timer or event handler: create them inside the `render` callback, not on first event. `computed` is exempt — it's lazy and disposes itself when it has no subscribers, so a class-field `computed` is fine.

`render` runs `setup` inside an `effectScope` detached from any enclosing effect — re-running an outer effect won't tear your scope down; only calling the returned `unmount` will.

## Why `For` items get their own scope

Each `<For>` item runs its render callback inside its own `effectScope`. `onCleanup` registered during render fires when that item's key leaves `each` — not only on full-list unmount. This keeps per-row resources (subscriptions, intervals, observers) bound to the row's actual lifetime.

```tsx
<For each={rows} by={(r) => r.id}>
  {(row) => {
    const off = subscribe(row.id);
    onCleanup(off); // fires when this row is removed
    return <li>{row.label}</li>;
  }}
</For>
```

## See also

* [Signals](/signals/) — `effect`, `effectScope`, `onCleanup`.
* [For (list rendering)](/elements/for/) — per-item scope details.
* [Custom elements overview](/custom-elements/) — lifecycle hooks.
