Overview

Every Mates factory is legal in some call sites and throws in others. This page is the full map: what you called, where it may run, SPA vs SSR, why, and who cleans up.

SPA (browser). ES module scope runs once per tab. A module-level atom is a shared client singleton — that is OK. The template still cannot create primitives (it re-runs).

SSR. Fullstack import(App) evaluates App.ts once per Node process, then reuses that module for every GET. A module-level atom/store/action would be one instance for all users. Guards throw: You can't call 'Atom' in ES module scope during server-side rendering. Move it into the component outer function so each request gets its own graph. Same idea as Next.js and other SSR frameworks.

The outer function is always the safe default (SPA and SSR). scope() class setup() counts as outer. The inner template and effect/memo callbacks must only read.

Restricted Usage

Scroll sideways on small screens. First column stays pinned.

allowed throws (runtime guard) allowed but you own cleanup / usually the wrong place Must — required call site

Reactive factories

API
SPA · module
SPA · outer
Template / effect
SSR · App.ts module
SSR · outer
Why
Cleanup / notes
atom / iAtom / atom.* SPA module = one shared client singleton. SSR App.ts is one module for every request — a module-level atom would leak one value to all users. No host cleanup required. Template / effect / memo throws (new instance every run).
store() SPA module is the shared-bag pattern. During SSR, create in the outer function so each request gets its own bag. Calling store() in SPA outer creates a per-component bag (like useState). Prefer module on SPA, outer on SSR.
asyncAtom / cacheAtom / formAtom Same as atom — factories that hold request data cannot live on a process-wide module during SSR. Inner timers/fetches: prefer outer so they die with the component. registerCleanup is a no-op without a host.
debouncedAtom / throttledAtom / delayAtom Timers on a module singleton outlive any one screen. SSR module would share timers across users. Outer: pending timeouts cancel via registerCleanup on unmount. Module: you own cancel.
LSAtom / lsStore / LSList (and SS*) Browser storage is per-origin, not per-user-on-the-server. A process cache during SSR would mix requests. Persistence is localStorage / sessionStorage. Prefer utils.lsStore in apps.
on / watch SSR module subscription would fan out to every GET. SPA module is allowed but has no host. Outer: unsub on unmount. Module: keep the returned cleanup and call it yourself.
effect / memo Same pollution rule as atoms. Callbacks must only read — creating primitives inside them throws. Outer: auto-dispose on unmount. Module: call the returned dispose(). Return a cleanup from effect(fn) for DOM/subscriptions.
setter / _ Reactive function wrapper — same create rules as other primitives. Cannot be created inside setup() (define as a class field first). Needs a host for view scheduling.
event / cleanupEvent / channel A module-level event on the server is one bus for every user. Component-scoped: subscribers via on() auto-clean. Module: you own unsub. channel.join is also guarded.
action / asyncAction / taskAction / fetchAction In-flight status/cache on a process singleton would mix users during SSR. Abort/cancel follows the action instance. Create in outer during SSR so teardown is per request.
molecule() A molecule is a bag of atoms — same pollution rule as creating those atoms at module scope during SSR. No extra cleanup. Prefer outer on SSR so each request gets its own bag.

Host-required (always outer)

API
SPA · module
SPA · outer
Template / effect
SSR · App.ts module
SSR · outer
Why
Cleanup / notes
scope() / getParentScope() Needs an active component host to register on the tree. Always outer — SPA and SSR. Torn down with the providing component. setup() on the class is also an allowed create site.
useUtils() Looks up MatesUtils on the host. Same store as utils, but still needs a component outer. Prefer utils outside components. Do not import pathAtom / themeAtom / lsStore as globals.
getPageStore / createPageStore Registry + cleanup are host-scoped. Module create would skip unmount cleanup and share one store on the server. Creator unmount removes the registry entry. Named unique classes required. SSR snapshots skip load().
createGlobalStore / getGlobalStore createGlobalStore needs a host so each SSR request gets its own window registry. createGlobalStore in App outer. getGlobalStore is a lookup — safe outside components after create.
useState / useForm Need a host for tracking / devtools. Not a process singleton. Per-component instance. Destroyed with the component — no extra cleanup.
ws() Must attach reconnect/close to a host. A module socket would never disconnect. Unmount disconnects. Prefer onSocket for handlers (auto-unsub). SSR: connection is a no-op.
useStore() Needs a host to subscribe the component to an existing store. No host at module scope. Unsub on unmount. The store itself is created separately (store() / class store).

Hooks & lifecycle

API
SPA · module
SPA · outer
Template / effect
SSR · App.ts module
SSR · outer
Why
Cleanup / notes
onMount / onCleanup / onPaint / onAllMount / onError Registered on the host. Template would add a new handler every render. SSR App.ts has no host. Return a cleanup from sync onMount, or call onCleanup. Async onMount cannot return cleanup — use onCleanup beside it. SSR awaits async mounts before HTML.
onWindow / onKeyDown / onResize / clipboard / visibility / … Listeners must be tied to a component so they are removed on unmount. Auto removeEventListener on unmount. Optional returned cleanup runs before the next fire and on unmount. SSR: most are no-ops.
onInterval / onTimeout / onCountdown Timers without a host leak. SSR module would start process-wide timers. Cleared on unmount. SSR returns noop handles — do not seed UI from ticks.
onSocket / onNavigate / onUpdate Subscribe on the host. onNavigate needs router scope from utils. Unsub on unmount. Create ws() in outer, then onSocket(fn, [socket]).

CSS-in-JS (must be module-level)

API
SPA · module
SPA · outer
Template / effect
SSR · App.ts module
SSR · outer
Why
Cleanup / notes
stylesheet() / globalCSS / keyframes / globalTheme Must Must Opposite of atoms: CSS-in-JS must stay at ES module scope so class names are stable. Calling in a component would mint new classes every mount. SSR render pass skips emitting CSS (the client sheet loads). No per-request pollution — this is a stylesheet, not user state.

Always OK to use in the template

API
SPA · module
SPA · outer
Template / effect
SSR · App.ts module
SSR · outer
Why
Cleanup / notes
html / svg / x() / view() Templates belong in the inner function. Module-level html is a static snippet, not a component. No cleanup. Read atoms in the inner function so tracking works.
utils (property access) Import of the proxy is always fine. Reading members looks up the window MatesUtils store and throws until renderApp. Do not destructure at module load. After mount, helpers / timers / fetch handlers may call utils.navigateTo(). One app per window.
Reading atom() / directives (classes, when, …) Reads in the template (and inside effect/memo) are tracked. Reads in outer/module are untracked snapshots. Nothing to clean up. Do not create primitives in directives’ render paths.

Exceptions & legacy exports

API
SPA · module
SPA · outer
Template / effect
SSR · App.ts module
SSR · outer
Why
Cleanup / notes
xTabEvent Process singleton by design (BroadcastChannel / storage). Not request state. Create in outer or module on the client. Do not put user data on it during SSR.
Exported themeAtom / titleAtom / pathAtom / lsStore Legacy process singletons. Importing them from mates during SSR App load can evaluate a shared instance + effect(). App code: utils (or useUtils()). The public exports exist for packages like mates-charts.

Error messages

SSR module: You can't call 'Atom' in ES module scope during server-side rendering (SSR). Call it in the outer function of the component instead. This is to make sure ES module scope is not polluted — that module graph is shared across all users.

Template: You can't call 'Atom' in the template function. You can only call it in the outer function of the component. The template reruns on every update…

Host-only: You can't call 'ws' at ES module scope or inside inner functions… You can only call it in the outer function of the component (or in a scope class setup())…

Outer function — SPA and SSR

Create atoms and register onMount in the outer function. Read chrome via utils after renderApp.

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

// Outer function — legal in SPA and during SSR App load
const App = () => {
const count = atom(0);
onMount(() => console.log('mounted once'));

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

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

CSS-in-JS must stay at module scope

stylesheet() is the opposite of atom(): it must run at ES module scope so class names stay stable. Calling it in a component would mint new classes every mount.

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

// CSS-in-JS must stay at ES module scope (SPA and SSR)
const { css, mount } = stylesheet();
mount();

const cl = css({
box: { padding: '12px', borderRadius: '8px' },
on: { outline: '2px solid #2dd4bf' },
});

const App = () => {
const on = atom(false);
return () => html`
<div class=${cl.box} ${classes([[on(), cl.on]])}>
<button class="btn-primary" @click=${() => on.set(v => !v)}>
Toggle outline
</button>
</div>
`;
};

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