Command Palette

Search for a command to run...

Hectal
PHASE 15Advanced ~29 min· topic 5 of 6

Topic 15.5

Atoms, Signals and Other Models

In one line

Besides one big store, state can be split into tiny independent atoms (Jotai, Recoil), tracked automatically with signals or observables (Preact Signals, MobX), or modelled as a state machine (XState); each model trades React's simple re-render rule for a different kind of precision.

Think of it like this

Three ways to run a classroom. In the first, the teacher keeps one big register of everything (a store, like Redux or Zustand). In the second, every student has their own small notebook, and you look only at the notebooks you care about (atoms). In the third, a helper quietly watches who reads which notebook and taps exactly those people on the shoulder when it changes (signals). And separately, the school bell follows a strict timetable: it can't ring "lunch over" before "lunch started" (a state machine).

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Atom
A tiny independent piece of state that components subscribe to on its own.
Derived atom
An atom whose value is computed from other atoms and updates when they change.
Signal
A value that remembers who read it and tells exactly those readers when it changes.
Automatic dependency tracking
The library notices which values your code reads while it runs, so you never list dependencies.
Fine-grained reactivity
Updating only the exact piece of UI that depends on a changed value, not a whole component.
Observable (MobX)
An object MobX watches, so code that reads it can react when it changes.
State machine
A model listing every state something can be in and which events move it from one state to another.
Guard
A condition that must be true for a state machine transition to happen.
Actor
In XState, a running machine (or other process) that receives events and can send them to others.

Step by step

01Atoms: bottom-up state

Each atom has a value, a list of listeners, and get/set/subscribe: an external store of one value. Components read an atom with useSyncExternalStore(atom.subscribe, atom.get). Because each atom is its own store, nothing needs a selector.

atoms.jsx (Jotai, real project)whole filejsx
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";

const priceAtom = atom(250);
const qtyAtom = atom(1);
const totalAtom = atom((get) => get(priceAtom) * get(qtyAtom)); // derived

function Qty() {
  const [qty, setQty] = useAtom(qtyAtom);
  return <button onClick={() => setQty((q) => q + 1)}>Qty: {qty}</button>;
}
function Total() {
  return <p>Total: ₹{useAtomValue(totalAtom)}</p>;
}
function Reset() {
  const setQty = useSetAtom(qtyAtom); // writes only: never re-renders on change
  return <button onClick={() => setQty(1)}>Reset</button>;
}

02Signals: reading is subscribing

A signal's getter checks "which effect is running right now?" and adds it to its subscribers. When the value is set, it re-runs exactly those effects. No component re-runs at all in a signals framework; in React you bridge the signal with useSyncExternalStore.

Signals: reading is subscribingdiagram
Rendering diagram…

03MobX: observable objects

A class becomes observable; mutations happen in actions; computed getters cache derived values; observer components re-render only when observables they read change.

CartStore.js (MobX, real project)whole filejsx
import { makeAutoObservable } from "mobx";
import { observer } from "mobx-react-lite";

class CartStore {
  items = [];
  constructor() { makeAutoObservable(this); }   // fields observable, methods actions, getters computed
  add(item) { this.items.push(item); }           // plain mutation inside an action
  get total() { return this.items.reduce((t, i) => t + i.price, 0); }
}
export const cart = new CartStore();

export const Total = observer(function Total() {
  return <p>Total: ₹{cart.total}</p>; // re-renders only when total changes
});

04State machines: events, states, transitions

List the states, and for each state the events it accepts. An event that isn't listed for the current state is ignored. In React, a transition table plus useReducer is a small machine; XState adds guards, side effects, timers, nesting and a visual editor.

fetchMachine.js (XState v5, real project)whole filejsx
import { assign, setup } from "xstate";
import { useMachine } from "@xstate/react";

const fetchMachine = setup({
  guards: { canRetry: ({ context }) => context.retries < 3 },
  actions: { countRetry: assign({ retries: ({ context }) => context.retries + 1 }) },
}).createMachine({
  id: "fetch",
  initial: "idle",
  context: { retries: 0 },
  states: {
    idle:    { on: { FETCH: "loading" } },
    loading: { on: { RESOLVE: "success", REJECT: "failure" } },
    success: { on: { FETCH: "loading" } },
    failure: { on: { RETRY: { guard: "canRetry", target: "loading", actions: "countRetry" } } },
  },
});

function Loader() {
  const [snapshot, send] = useMachine(fetchMachine);
  return snapshot.matches("loading")
    ? <p>Loading…</p>
    : <button onClick={() => send({ type: "FETCH" })}>Load</button>;
}
State machines: events, states, transitionsdiagram
Rendering diagram…

Try it yourself

  1. 1

    Add a write-only component

    In the atoms example, add function Reset() { console.log(" render Reset"); return <button onClick={() => qtyAtom.set(1)}>Reset</button>; } to the root. Predict: does it render when you click Qty? It never reads an atom, so it never subscribes and never re-renders. This is Jotai's useSetAtom.

  2. 2

    Read a signal outside an effect

    In the signal example, add console.log(first.value); at the top level, then first.value = "Ravi";. Predict which effects run. Effect A runs (it read first while running); the top-level read subscribed nothing, because no effect was running.

  3. 3

    Add a guard

    In the state machine, allow at most two retries: keep a retries count next to the state in the reducer ({ value, retries }) and ignore RETRY when retries === 2. This is what XState's guard does.

Code & diagrams

Hand-made atoms: each reader subscribes to its own piece React 18+ New tab

No selectors anywhere: each component subscribes to whole atoms, and atoms are small. Changing `qtyAtom` re-renders `Qty` and, through the derived `totalAtom`, `Total`. `ThemeName` renders only when the theme changes. Jotai works on the same idea, with automatic tracking for derived atoms and a store per `<Provider>`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

  render Qty
  render Total
  render ThemeName
click Qty
  render Qty
  render Total
set theme
  render ThemeName
A tiny signal: automatic tracking, then a bridge into React React 18+ New tab

Nobody listed dependencies: each effect subscribed to exactly the signals it read while running. Click the button: effect B logs again, because the React handler sets the same signal. Real libraries (Preact Signals, SolidJS) add batching, computed values, cleanup and glitch-free ordering on top of this idea.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

effect A: full name = Asha Rao
effect B: clicks = 0
-- set last
effect A: full name = Asha Kumar
-- set clicks
effect B: clicks = 1
-- set clicks to the same value
A state machine with a transition table and useReducer New tab

`RETRY` while loading and `RESOLVE` after a failure are not in the table, so the state doesn't change (React still called `Loader` once for the ignored `RETRY` before seeing the state was the same, which is allowed). Greyed buttons show which events the current state accepts.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

  state: idle
send FETCH
  state: loading
send RETRY
  state: loading
send REJECT
  state: failure
send RESOLVE
  state: failure
send RETRY
  state: loading
send RESOLVE
  state: success

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Reading a signal during render without a bridge

In the signal example, write function Clicks() { return <button onClick={() => { clicks.value++; }}>Clicked {clicks.value} times</button>; } (no useSignal).

terminal
$ Preview
── what you'll see ──
Clicked 0 times (stays at 0 however often you click)

Break #2

Boolean flags instead of a machine

Model a request with isLoading, isError and isSuccess booleans, and set them in three different handlers.

terminal
$ Preview
── what you'll see ──
Loading…
Something went wrong
Saved!

Myth vs fact

Myth

Signals would make React faster, so React will switch to them.

Fact

The React team has chosen to keep the snapshot model (UI = f(state)) that concurrent rendering depends on, and to reduce wasted renders with the React Compiler instead.

Myth

Atoms are just a fancier useState.

Fact

Atoms live outside components, can be shared anywhere, derived from each other and read without prop drilling or selectors. They're closer to many tiny external stores.

Myth

State machines are only for games or hardware.

Fact

Any UI with steps and statuses is a state machine already: forms, checkouts, uploads, auth flows. Writing it down explicitly removes impossible states.

Myth

Recoil is the official React state library.

Fact

Recoil came from Meta, but it was never part of React, and it was archived in early 2025. Jotai is the actively maintained library with a similar atom model.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Atom libraries scale differently from stores: thousands of atoms are fine (each with its own subscribers), and Jotai's atomFamily (in jotai/utils) or a Map of atoms gives per-item state, e.g. one atom per row, so editing a row re-renders only that row with no selector work at all.

  • ▸

    Signals frameworks avoid the virtual DOM diff for updates, but pay elsewhere: tracking costs memory, "glitches" (seeing half-updated derived values) need careful scheduling, and code must read values inside tracked scopes. React chose predictability and concurrency; the Compiler (Topic 16.5) closes much of the speed gap for typical apps.

  • ▸

    MobX can feel like magic until it doesn't: destructuring an observable in a parent (const { name } = user) and passing name down means the child doesn't track user.name. Pass the observable object and dereference late, inside observer components.

  • ▸

    XState v5 treats everything as an actor: machines, promises and callbacks. A whole app's logic can run as a system of actors outside React, with components only sending events and reading snapshots via useSelector(actorRef, selector) from @xstate/react, which again is a selector-based subscription.

Remember this

  1. 1

    Atoms are tiny independent pieces of state. Instead of one store with selectors, you create many small units (priceAtom, qtyAtom, themeAtom) and components subscribe to exactly the atoms they read, so a change re-renders only those readers, with no selectors to write. Derived atoms compute a value from other atoms (totalAtom = price × qty) and update when their sources change. State is built bottom-up from atoms, not cut top-down from one big object. The live example below builds a 15-line atom library on useSyncExternalStore to show there is no magic.

  2. 2

    Jotai is the leading atom library: const countAtom = atom(0), then const [count, setCount] = useAtom(countAtom) in a component, useAtomValue to only read, useSetAtom to only write (that component doesn't re-render on changes). A derived atom is atom((get) => get(priceAtom) * get(qtyAtom)); Jotai tracks which atoms get touched, so you don't list dependencies. Atoms are only definitions: values live in a store (a default one, or one per <Provider>), which makes per-request state in server rendering easy. Recoil, Meta's earlier atom library, introduced the idea to many React developers but was archived by Meta in early 2025 and is no longer maintained; don't start new projects with it, and plan a move (often to Jotai, whose API is close) if you use it.

  3. 3

    Signals are values that know who reads them. A signal holds a value; when code reads signal.value inside an effect or a computed value, the signal records that reader automatically (automatic dependency tracking). When the value changes, only those readers re-run. This is fine-grained reactivity: SolidJS, Preact Signals, Vue's ref, Angular signals and Svelte 5 runes all use it, and a TC39 proposal aims to add signals to JavaScript itself. Frameworks built on signals can update a single text node without re-running any component function.

  4. 4

    Why React doesn't use signals. React's model is UI = f(state): each render is a pure function call that sees one consistent snapshot of props and state. That model is what makes concurrent rendering (pausing and restarting renders, Topic 16.7), transitions, Suspense and server components work: React can render a version of the UI "off-screen" without changing what's shown. Signals are mutable cells read at any time, so different parts of a render could see different values (tearing). React's answer to "re-renders do too much work" is skipping work automatically with the React Compiler (Topic 16.5) instead of changing the model. Community adapters that put signals into React exist, but they reach into React's internals or need a build transform, so treat them with care. To use any signal-like source safely, subscribe through useSyncExternalStore, as the live example does.

  5. 5

    MobX applies the same tracking idea to whole objects: you write a normal class or object, make it observable (makeAutoObservable(this)), change it by plain mutation inside actions (this.items.push(item)), and derive values with computed getters. Wrapping a component in observer (from mobx-react-lite) makes it track which observables it read during render and re-render only when those change. It feels like plain object-oriented code and gives precise updates; the price is mutable, "magic" data flow that is harder to trace than Redux's explicit actions.

  6. 6

    State machines model which states exist and which events may move between them. A fetch can be idle, loading, success or failure; RESOLVE is only allowed while loading. Writing that as a table makes impossible states impossible: no isLoading && isError at the same time, no double submit. You already did a small version with a status variable (Topic 6.1) and a reducer (Topic 9.4). XState (version 5) is the full tool: setup({...}).createMachine({...}) with states, events, guards (conditions on a transition), actions (side effects on transitions), context (extra data), nested and parallel states, timers, and actors (running machines that talk to each other). useMachine(machine) from @xstate/react returns [snapshot, send]. It also draws your machine as a diagram (Stately's visual editor). Use it for complex flows: checkout, multi-step wizards, media players, upload pipelines.

Explain it without notes

01

How does an atom-based library like Jotai differ from a single-store library like Zustand or Redux?

02

What are signals, and why doesn't React use them for state?

03

How does MobX decide which components to re-render?

04

What problem do state machines solve in UI code, and when would you reach for XState?

Practice

01

Using the hand-made atom from the live example, add a discountAtom (0 to 1) and make totalAtom apply it. Which components re-render when the discount changes?

02

Write a transition table for a traffic light (green → yellow → red → green on TIMER, and any state → flashing on FAULT, flashing → red on RESET) and a reducer that uses it.

Trade-offs

  • ↔

    Atoms and signals give precise updates with little code, but state is spread over many small definitions, which can be harder to see as a whole than one store in DevTools.

  • ↔

    State machines remove impossible states and document behaviour, but they take more upfront modelling; for a simple toggle or a single fetch, a status value is enough.

  • ↔

    MobX's mutable style is quick to write and very precise, but its implicit tracking is harder to debug and doesn't follow React's immutable-snapshot conventions, so teams mixing styles get confused.

Done when you can

  • I can explain atoms and derived atoms, and write a tiny atom on useSyncExternalStore.

  • I know Recoil is archived and Jotai is the maintained atom library.

  • I can explain signals, automatic tracking and why React keeps snapshots.

  • I can describe how MobX's observer decides what to re-render.

  • I can model a flow as a state machine with a transition table.

  • I know when XState is worth adding.