Overview

Mates has four execution contexts. Rules for creating primitives, tracking reads, and cleanup all depend on which one you are in.

1. ES module scope — top of the file, outside any function. Runs once on import. On a SPA, fine for shared atoms/events (no component lifecycle — __subscribe cleanup is manual). During SSR, App.ts is evaluated once per Node process and reused for every GET — module-level atoms/stores/actions throw. Full table: Restricted Usage.

2. Component outer (setup) — the outer function of a component. Runs once on mount. The safe default in SPA and SSR. Create atoms, effects, events, and on* hooks here. Lifecycle-aware APIs (on(), effect(), on-hooks) auto-clean on unmount.

3. Component inner (template) — the function returned by the outer. Re-runs on every reactive update. Read atoms here (tracked). Never create primitives or call on-hooks — that throws.

4. Handler / callback — event handlers, action callbacks, timers, promises. Safe to mutate existing state. Creating primitives works but is discouraged (no lifecycle). Creating inside effect()/memo() callbacks throws.

Creation rules (what can be created where)

Primitive
Module / Outer
Template / Handler
atom / memo / effect / throws in template · don't create in handlers · throws inside effect/memo callbacks
event / channel / action / throws in template · don't create in handlers
scope / useScope / Outer only — needs an active component host
On hooks (onMount, on, …) / Outer only — throws in template; no host in handlers

Correct: create in outer, read in template

Atoms live in the setup closure. The template only reads count() — that read is tracked, so the button label updates on set.

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

const App = () => {
// ✅ Outer: create once for the component lifetime
const count = atom(0);

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

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

Anti-pattern avoided: never atom() in template

Do not call atom() (or onMount) inside the inner function — it throws. Create in outer, read in template, mutate in handlers.

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

const App = () => {
// ❌ Never: atom() / onMount() inside the template (throws)
// ✅ Always: create in outer, read in template, mutate in handlers
const count = atom(0);

return () => html`
<div class="m-col m-gap">
<p class="muted m-t-5">Template only reads — no atom()</p>
<h1>${count()}</h1>
<button class="btn-primary" @click=${() => count.set(n => n + 1)}>
Inc
</button>
</div>
`;
};

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