Overview

atom is the foundational reactive unit in Mates. It is a plain callable function object — calling it reads the value and registers a reactive dependency so the enclosing template, memo, or effect re-runs whenever the value changes.

Create atoms in the component outer function (or at module level for app-wide state). Read them with count(), count.get(), or count.val — all three are equivalent reactive reads. Write with .set() (value or updater) or .update() for in-place object/array mutation.

Passing a function to atom() creates a derived atom. The function runs immediately; any atoms read inside become dependencies, and the derived value recomputes whenever they change. Derived atoms can chain.

Same-value writes still notify. Unlike some frameworks, count.set(count()) is treated as a change and re-runs subscribers. Use a silent write (set(v, true)) when you must mutate without notifying.

atom vs nearby APIs

API
Mutability
Prefer when
atom Mutable + notify Default for reactive values and small objects.
iAtom / signal Deep-frozen on set Configs / read-models where in-place mutation should fail.
memo(fn) Derived only Same runner as atom(() => …); clearer “computed” intent.
asyncAtom Async data + status Keyed fetch/refetch with loading/error — not a plain value cell.

Creating and reading atoms

Atoms are reactive variables. Read them in the template with atom() — any template that reads an atom will re-render when it changes.

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

const App = () => {
const count = atom(0);
const name = atom("Ada");

return () => html`
<div class="m-col m-gap">
<div>
<h1>${count()}</h1>
<p class="muted m-t-5">Hello, ${name()}!</p>
</div>
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button class="btn-danger" @click=${() => count.set(n => n - 1)}>−</button>
<button @click=${() => count.set(0)}>Reset</button>
<button class="btn-primary" @click=${() => count.set(n => n + 1)}>+</button>
</div>
<input
.value=${name()}
@input=${(e) => name.set(e.target.value)}
placeholder="Enter your name..."
/>
</div>
`;
};

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

Derived atoms

Pass a function to atom() to create a computed value that auto-recomputes when its dependencies change.

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

const App = () => {
const celsius = atom(0);
const fahrenheit = atom(() => Math.round((celsius() * 9) / 5 + 32));
const kelvin = atom(() => Math.round(celsius() + 273.15));

return () => html`
<div class="m-col m-gap">
<label class="m-col m-items-start m-gap-sm">
Celsius: <strong>${celsius()}°C</strong>
<input type="range" style=${"--v:" + celsius()} min="0" max="100"
.value=${celsius()}
@input=${(e) => celsius.set(+e.target.value)}
/>
</label>
<div class="m-grid m-grid-cols-3 m-gap">
<div class="card card-body center">
<span class="overline">Fahrenheit</span>
<span class="card-title">${fahrenheit()}°F</span>
</div>
<div class="card card-body center">
<span class="overline">Kelvin</span>
<span class="card-title">${kelvin()} K</span>
</div>
</div>
</div>
`;
};

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

In-place update for objects

profile.update(draft => …) mutates the live object/array and notifies. Prefer this for nested field edits; use set() to replace the whole value.

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

const App = () => {
const profile = atom({ name: "Ada", score: 0 });

return () => html`
<div class="m-col m-gap">
<p><strong>${profile().name}</strong> — score ${profile().score}</p>
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button class="btn-primary" @click=${() =>
profile.update(p => { p.score += 10; })
}>+10 score</button>
<button @click=${() =>
profile.update(p => { p.name = p.name === "Ada" ? "Grace" : "Ada"; })
}>Rename</button>
</div>
</div>
`;
};

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

Lock and unlock writes

While locked, set/update are blocked. Unlock with the same passcode to write again.

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

const App = () => {
const count = atom(0);
const locked = atom(false);

const toggleLock = () => {
if (locked()) { count.unlock(); locked.set(false); }
else { count.lock(); locked.set(true); }
};

return () => html`
<div class="m-col m-gap">
<h1>${count()}</h1>
<p class="muted m-t-5">${locked() ? "Locked — set() is blocked" : "Unlocked"}</p>
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button class="btn-primary" @click=${() => count.set(n => n + 1)}>+1</button>
<button @click=${toggleLock}>${locked() ? "Unlock" : "Lock"}</button>
</div>
</div>
`;
};

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