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.

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

const ProfileCard = () => {
// OUTER — setup phase, runs once
const name = atom('Loading…');
const role = atom('');
const liked = atom(false);

onMount(() => {
// Simulate an async data fetch after the component mounts
setTimeout(() => {
name.set('Ada Lovelace');
role.set('Mathematician & Computer Scientist');
}, 700);
});

// INNER — template phase, runs on every reactive update
return () => html`
<div class="card card-body center">
<div class="avatar avatar-lg m-x-auto m-b-20">AL</div>
<h2>${name()}</h2>
<p class="label m-b-10">${role()}</p>
<button
class=${liked() ? 'btn-primary' : 'btn-ghost'}
@click=${() => liked.set(v => !v)}
>
${liked() ? '❤️ Liked' : '🤍 Like'}
</button>
</div>
`;
};

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

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.

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

const AtomDemo = () => {
const count = atom(0);

// Derived atoms — recompute automatically when count changes
const doubled = atom(() => count() * 2);
const status = atom(() =>
count() === 0 ? 'zero' : count() > 0 ? 'positive' : 'negative'
);

return () => html`
<div class="m-col m-gap">
<div class="m-grid m-grid-cols-3 m-gap">
<div class="card card-body center">
<span class="overline">count</span>
<span class="card-title">${count()}</span>
</div>
<div class="card card-body center">
<span class="overline">doubled</span>
<span class="card-title">${doubled()}</span>
</div>
<div class="card card-body center">
<span class="overline">status</span>
<span class="tag ${status() === 'zero' ? '' : status() === 'positive' ? 'tag-success' : 'tag-danger'}">${status()}</span>
</div>
</div>
<div class="m-flex m-items-center m-justify-center m-gap-sm m-t-10">
<button class="btn-danger" @click=${() => count.set(n => n - 1)}>−1</button>
<button @click=${() => count.set(0)}>Reset</button>
<button class="btn-primary" @click=${() => count.set(n => n + 1)}>+1</button>
</div>
</div>
`;
};

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.

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

// Child — read props via propsFn() inside the inner template
const StatusBadge = (propsFn) => {
return () => {
const status = propsFn().status;
const cls = { online:'tag-success', away:'tag-warning', offline:'tag' }[status] ?? 'tag';
return html`
<span class="tag ${cls}">${status}</span>
`;
};
};

// Parent embeds StatusBadge with x(Component, props)
const UserCard = (propsFn) => {
return () => html`
<div class="card card-body m-flex m-gap-sm m-items-center m-justify-between m-b-10">
<span>${propsFn().name}</span>
${x(StatusBadge, { status: propsFn().status })}
</div>
`;
};

const App = () => {
const users = atom([
{ name: 'Ada Lovelace', status: 'online' },
{ name: 'Alan Turing', status: 'away' },
{ name: 'Grace Hopper', status: 'offline' },
]);

return () => html`
<div class="m-col m-gap">
<h3>Team</h3>
${users().map(u => x(UserCard, { name: u.name, status: u.status }))}
</div>
`;

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>
  `;
};