Overview

A Mates component is a function that returns a function. The outer function is the setup phase: it runs exactly once when the component mounts. Create atoms, define event handlers, register onMount / onCleanup, and kick off one-time work there.

The inner function is the render phase: it re-runs every time a reactive dependency changes. Reading an atom with count() or calling propsFn() inside it registers that dependency — no dependency arrays, no manual wiring. Its only job is to return an html template; keep it free of side effects.

This split is why Mates is fast: the setup never re-runs, and only the components whose atoms actually changed re-render their inner functions. There is no virtual DOM diffing — the reactive graph drives targeted DOM patches directly.

What goes where — a placement guide

Code
Layer
Why
const count = atom(0) Setup Created once. Every render reads the same atom.
onMount(() => { … }) Setup Lifecycle hooks run once per mount. Return a cleanup function to tear down.
const increment = () => … Setup Handlers are created once and close over atoms — no stale closures.
loadData() / fetch Setup One-time work. Better: an asyncAction for loading/error state.
propsFn() Render Returns the latest props on every render.
count() inside html Render Reading an atom is what registers the dependency that drives re-renders.
x(Child, props) Render Embedding children is render work.
setInterval(…) Render A new interval on every update. Use onInterval in setup.
count.set(…) Render Writing during render re-triggers the render — infinite loop.
const { name } = propsFn() Setup Destructured once = frozen forever. Destructure inside the render.

Watch the two layers run

Press Trigger update: the render counter climbs while setup stays at . The outer function ran exactly once; every click re-runs only the inner function.

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

const TwoLayers = () => {
// SETUP — outer function. Runs ONCE when the component mounts.
const count = atom(0);
const renderRuns = atom(1); // the first render happens right after setup

const trigger = () => {
count.set((n) => n + 1);
renderRuns.set((r) => r + 1); // only the INNER function re-runs
};

// RENDER — inner function. Runs on every reactive update.
return () => html`
<div class="card card-body center">
<p>${count()}</p>
<button class="btn-primary" @click=${trigger}>Trigger update</button>
<div class="m-flex m-items-center m-justify-center m-gap-sm m-t-10">
<span class="tag tag-soft">setup ran: 1×</span>
<span class="tag tag-soft">render ran: ${renderRuns()}×</span>
</div>
</div>
`;
};

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

A complete component

The same model in a realistic shape: state and handlers in setup, a template that reads atoms in render.

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

const Counter = () => {
// OUTER — runs once on mount: create atoms and handlers
const count = atom(0);
const step = atom(1);

// INNER — runs on every reactive update: read atoms, return html
return () => html`
<div class="card card-body center">
<p>${count()}</p>
<div class="m-flex m-items-center m-justify-center m-gap-sm m-b-10">
<button @click=${() => count.set(n => n - step())}>−${step()}</button>
<button @click=${() => count.set(0)}>Reset</button>
<button @click=${() => count.set(n => n + step())}>+${step()}</button>
</div>
<label class="hint muted">
Step: <input type="number" .value=${step()} min="1" class="m-l-10"
@input=${e => step.set(Number(e.target.value))} />
</label>
</div>
`;
};

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

Component with props

Props arrive as the first argument — propsFn — a function you call inside the render to get the latest values. Each time the parent re-renders, propsFn() returns the updated props.

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

// propsFn is a function — call it inside the INNER function to stay reactive
const Card = (propsFn) => {
return () => html`
<div class="card">
<h3 class="m-b-5">${propsFn().title}</h3>
<p class="muted">${propsFn().body}</p>
</div>
`;
};

const App = () => {
const title = atom('Component Model');
const body = atom('Outer runs once; inner runs on every update.');
return () => html`
<div class="m-col m-gap-sm">
${x(Card, { title: title(), body: body() })}
<input .value=${title()} @input=${e => title.set(e.target.value)}
/>
</div>
`;
};

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