Coming from React
A concept-by-concept mapping from React to Mates — hooks become hooks, JSX becomes html``, and state stays mutable.
What changes (and what doesn't)
If you know React, you already know most of the shape: components, props, effects, lists with keys, and a router. Mates drops the virtual DOM, the immutability tax, and the JSX compile step. The hard part is the two-layer component — outer setup runs once; inner template re-runs on atom reads. Internalize that first via Mental Model.
Concept mapping
|
React
|
Mates
|
Difference
|
|---|---|---|
useState
|
atom / iAtom
|
Mutable via .set() — no setter pairs, no immutability
|
useEffect
|
effect()
| Auto-tracks what it reads — no dependency array |
useMemo
|
memo()
| Derived atom; recomputes when inputs change |
useRef
|
ref() / setRef
| Template-first: bind the element in the template |
useContext
|
Scopes (useScope) / x-provider
| Plain classes shared down the tree — scopes, context |
useReducer
|
action / store
| Trackable functions instead of dispatch tables |
useLayoutEffect
|
onMount / onPaint
| Explicit lifecycle timing |
useEffect cleanup
|
onMount return value
| Return the teardown function |
| JSX |
html``
| Tagged templates — no build step |
| React Router |
Router / animatedRouter
|
One router; nested screens via paramsAtom()
|
| React Query / SWR |
asyncAction
| Loading/error/data + cache + polling built in |
A component, side by side
import { useState, useEffect } from "react";
export function Timer({ start = 0 }) {
const [seconds, setSeconds] = useState(start);
useEffect(() => {
const id = setInterval(() => setSeconds((s) => s + 1), 1000);
return () => clearInterval(id);
}, []);
return <h3>Seconds: {seconds}</h3>;
}
import { atom, html, onInterval } from "mates";
import type { Props } from "mates";
export const Timer = (propsFn: Props<{ start?: number }>) => {
const seconds = atom(propsFn().start ?? 0);
onInterval(() => seconds.set((s) => s + 1), 1000);
return () => html`<h3>Seconds: ${seconds()}</h3>`;
};
- The outer function replaces the component body — it runs once, not on every render
- The inner function is the render — re-runs only when atoms it reads change
onIntervalcleans itself up on unmount — no manualclearInterval- Read live props with
propsFn()inside the template if parents can change them
Rethinking state updates
React trains you to treat state as immutable. Mates drops that rule — stores and keyed collections are mutated directly, and only the DOM parts that read the changed state are patched:
// React: copy everything to change one thing
setTodos((prev) =>
prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t)),
);
// Mates store: mutate through methods — re-renders are automatic
const todoStore = store({
todos: [] as Todo[],
toggle(id: string) {
const todo = this.todos.find((t) => t.id === id);
if (todo) todo.done = !todo.done;
},
});
const List = () => {
const [state] = todoStore();
return () => html`…${state.todos.map(…)}…`;
};
Keyed collections follow the same rule — setAtom and mapAtom expose the native Set/Map API and track every mutation:
const cache = mapAtom<string, Todo>([]);
cache.set(id, todo); // reactive
cache.delete(id); // reactive
Events and lists
import { html, repeat } from "mates";
const List = (propsFn) => {
const todos = propsFn().todos;
return () => html`
<ul>
${repeat(
todos(),
(todo) => todo.id,
(todo) => html`
<li @click=${() => toggle(todo)}>${todo.title}</li>
`,
)}
</ul>
`;
};
@clickin templates replacesonClickrepeat()keyed by id is thekey={}equivalent — and powers the fastest keyed results in the js-framework-benchmark- Event handlers close over the outer scope — no stale-closure problem to reason about
Migration checklist
- Move state out of render — declare atoms in the outer function
- Replace
useEffectwitheffect()(reactive) oron*hooks (lifecycle) - Replace context providers with scope classes /
x-provider - Replace fetch + loading flags with
asyncAction - Delete your date library — date utils ship in the core package
- Delete your toast/tooltip/modal library — portals and overlays are built in; UI kit at ui.mates-js.dev
- Skim Patterns & Pitfalls before the first PR review