MATES / Docs

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

React
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>;
}
Mates
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
  • onInterval cleans itself up on unmount — no manual clearInterval
  • 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

Lists and events
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>
  `;
};
  • @click in templates replaces onClick
  • repeat() keyed by id is the key={} 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 useEffect with effect() (reactive) or on* 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