Overview

memo(fn) creates a reactive computed atom. fn runs immediately; atoms read inside become dependencies; whenever they change, fn re-runs and the returned atom updates.

The return value is a full AtomType<T> — callable, subscribable, and chainable. It is functionally identical to atom(() => …) (implemented separately to avoid a circular import). Prefer memo at module level or when the call site should read as “computed”; prefer atom(fn) for terse inline derived values in components.

Create memos in the component outer function or at module level — never inside the template return, and never inside another memo/effect callback.

memo(fn) vs atom(() => …)

Form
Behavior
Prefer when
memo(fn) Same reactive runner Module-level or shared derived values — clearer “computed” intent.
atom(() => expr) Functionally identical Inline derived atoms inside component outer functions (slightly terser).

Derived values with memo()

memo() computes from tracked atoms and only re-runs when a dependency changes — not on every render.

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

const App = () => {
const width = atom(200);
const height = atom(150);
const area = memo(() => width() * height());
const perimeter = memo(() => 2 * (width() + height()));

return () => html`
<div class="m-col m-gap">
<label class="m-col m-items-start m-gap-sm">
Width: <strong>${width()}px</strong>
<input type="range" min="50" max="400"
.value=${width()}
@input=${(e) => width.set(+e.target.value)} />
</label>
<label class="m-col m-items-start m-gap-sm">
Height: <strong>${height()}px</strong>
<input type="range" min="50" max="300"
.value=${height()}
@input=${(e) => height.set(+e.target.value)} />
</label>
<div class="m-grid m-grid-cols-3 m-gap">
<div class="card card-body center">
<span class="overline">Area</span>
<span class="card-title">${area()} px²</span>
</div>
<div class="card card-body center">
<span class="overline">Perimeter</span>
<span class="card-title">${perimeter()} px</span>
</div>
</div>
</div>
`;
};

memo() vs atom(() => …)

Both forms share the same runner. Values stay in lockstep — choose the call-site style you prefer.

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

const App = () => {
const n = atom(4);
// Same runner — pick the form that reads clearest at the call site
const viaMemo = memo(() => n() * n());
const viaAtom = atom(() => n() * n());

return () => html`
<div class="m-col m-gap">
<h1>${n()}</h1>
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button class="btn-danger" @click=${() => n.set(v => v - 1)}>−</button>
<button class="btn-primary" @click=${() => n.set(v => v + 1)}>+</button>
</div>
<div class="m-grid m-grid-cols-3 m-gap">
<div class="card card-body center">
<span class="overline">memo()</span>
<span class="card-title">${viaMemo()}</span>
</div>
<div class="card card-body center">
<span class="overline">atom(fn)</span>
<span class="card-title">${viaAtom()}</span>
</div>
</div>
</div>
`;
};

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