Overview
Mates components are closures: an outer setup function runs once on mount, and an inner template function re-runs whenever reactive dependencies change. There is no virtual DOM and no JSX compiler — you write html`…` templates exported from mates.
State is explicit: atom, effect, memo, and friends. Read atoms by calling them (count()); write with count.set(…). Create reactive primitives only in the outer function so they live for the component lifetime.
Compose UI with x(Child, props) — props arrive as a Props<T> getter. Call propsFn() inside the inner function so updates stay reactive. The full context rules live on Mental Model.
Core pieces at a glance
|
Piece
|
Role
|
Prefer when
|
|---|---|---|
atom
| Reactive value |
Local or shared state; derived via atom(() => …).
|
html`…`
| Template | Return from the inner function; lit-html under the hood. |
x()
| Composition | Embed a child component with typed props. |
renderApp
| Mount | Root entry — scopes + first paint. |
The Two-Layer Component Model
Outer setup runs once (atoms + onMount). Inner template re-runs when atoms change.
The outer function is your component's constructor. Everything you create there is tied to the component's lifetime and cleaned up on unmount — atoms, effects, lifecycle hooks, event listeners, timers, and async actions all belong here.
The inner function is your render function. It must be
a pure reactive read — call atom() to read values and they
will be tracked as dependencies. When any dependency changes, only the
inner function re-runs; the outer function is never repeated.
Reactive State with atom
Pass a function to atom() for a derived value — it recomputes when dependencies change.
How atoms work
atom(value) creates a reactive value. Read it by calling
it — count() — or via count.get(). Write it
with count.set(nextValue) or
count.set(prev => prev + 1) for an updater function.
Passing a function creates a derived atom:
atom(() => count() * 2). It tracks its own reactive
dependencies automatically and recomputes when they change — no
separate memo() boilerplate required.
Composing Components with x()
x(Component, props) embeds a child. Props are a plain object; Mates wraps them in a reactive propsFn.
TypeScript Types
Components receive a propsFn: Props<T> argument — a
zero-arg function that returns T. Call
propsFn() inside the inner (template) function so
that prop changes trigger re-renders. The return type of the inner
function can be annotated as TemplateResult.
import { html } from 'mates';
import type { Props, TemplateResult } from 'mates';
// Props<T> is a zero-arg function that returns T.
// Call propsFn() inside the inner (template) function to stay reactive.
const Greeting = (propsFn: Props<{ name: string; greeting?: string }>) => {
return (): TemplateResult => html`
<p>${propsFn().greeting ?? 'Hello'}, ${propsFn().name}!</p>
`;
};
// Props with optional fields and a callback
const Button = (propsFn: Props<{
label: string;
variant?: 'primary' | 'ghost';
onClick: () => void;
}>) => {
return (): TemplateResult => html`
<button
class="btn-${propsFn().variant ?? 'primary'}"
@click=${propsFn().onClick}
>
${propsFn().label}
</button>
`;
};