Skip to content
cccskills
FreePublished on GitHub

web-state-jotai

Atomic state management with auto-dependency tracking

The English summary is being prepared. Showing the original description.

See how to install

Files included (7)

  • SKILL.md10.3 KB
  • examples/async.md6.0 KB
  • examples/core.md5.8 KB
  • examples/persistence.md6.4 KB
  • examples/testing.md4.6 KB
  • metadata.yaml336 B
  • reference.md5.1 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Jotai Patterns

Quick Guide: Jotai builds state bottom-up out of atoms, each tracking its own dependencies, so a component re-renders only for the atoms it read. Atoms are defined at module level; defining one inside a component makes a new atom on every render and the state never survives. Async atoms suspend by default, so their consumers need a Suspense boundary — loadable() and unwrap() are the ways out of that. atomFamily from jotai/utils is deprecated in favour of the jotai-family package.

Detailed Resources:


Which path applies

  • The value is available synchronously — primitive and derived atoms, no boundary needed; follow examples/core.md.
  • The value arrives from a promise — the atom suspends, so the consumer needs a Suspense boundary or a wrapper that turns suspension into a value; follow examples/async.md.
  • The value has to outlive the tab — storage-backed atoms, with a first-render caveat; follow examples/persistence.md.

<critical_requirements>

Before writing Jotai code

Define atoms at module level. An atom is an identity, not a value — one created during render is a new identity each time, so the state resets on every render and the component appears not to respond.

Give async atom consumers a Suspense boundary. An async atom suspends the moment it is read, and a suspension with nothing to catch it propagates as an error.

Put multi-atom updates behind a write-only atom. The updates then land together, and the logic is reachable from anywhere without a component around it.

Use the jotai-family package for parameterised atoms. atomFamily in jotai/utils is deprecated, and families of either kind hold every atom they create until told otherwise.

</critical_requirements>


Auto-detection: atom, useAtom, useAtomValue, useSetAtom, atomWithStorage, atomWithReset, splitAtom, selectAtom, loadable, unwrap, createStore, RESET, jotai/utils, jotai-family, Provider store

Applies to:

  • Composing state out of primitive, derived, write-only and read-write atoms
  • Async atoms, and choosing between suspending and handling the loading state
  • Persisting an atom, and splitting an array into per-item atoms
  • Store and Provider scoping, including isolation between tests

Handled elsewhere:

  • Server data — caching, invalidation and refetching belong to whatever owns the network; an async atom fetches once and has no opinion about staleness
  • Single-value component state — one boolean in one component needs no atom
  • State that should survive a paste of the URL — filters and pagination belong in the address bar

<philosophy>

Atoms are cells in a spreadsheet. A primitive atom holds a value, a derived atom is a formula over other cells, and changing a cell recalculates exactly the formulas that read it — nothing declares a dependency, because the dependency is whatever get was called on while the formula ran.

Two things follow. State is built bottom-up from small pieces rather than carved out of one large object, so the re-render boundary comes for free instead of from memoisation. And an atom is an identity, not a container: the value lives in a store, and the atom is the key. That is why the same atom read under two Providers gives two values, and why an atom created inside render is a different key each time.

</philosophy>

<decision_framework>

Which atom shape

Does it hold a value of its own?
├─ YES -> primitive: atom(initialValue)
└─ NO -> Is it computed from other atoms?
    ├─ YES -> Does it also accept writes?
    │   ├─ YES -> read-write: atom(read, write)
    │   └─ NO  -> derived: atom((get) => ...)
    └─ NO -> it only performs updates
        └─ write-only: atom(null, (get, set) => ...)

Async atoms, with or without Suspense

Should the consumer suspend while the value is pending?
├─ YES -> read the async atom directly, under a Suspense boundary
└─ NO -> Does the UI need to distinguish loading from error?
    ├─ YES -> loadable(asyncAtom) — a discriminated union on `state`
    └─ NO  -> unwrap(asyncAtom, fallback) — a plain value throughout

Arrays

Does each item update independently?
├─ YES -> splitAtom, with a keyExtractor where items have ids
└─ NO  -> a single array atom is enough

Reach for a derived atom before selectAtom. The official docs call selectAtom an escape hatch; a derived atom expresses the same read with the dependency tracking that is the point of the library.

</decision_framework>


<patterns>

Core patterns

Pattern 1: Primitive Atoms

A single value, typed by inference. Explicit types are for unions and nullables.

import { atom } from "jotai";

const countAtom = atom(0);
const userAtom = atom<User | null>(null);
const themeAtom = atom<"light" | "dark" | "system">("light");

Full code: examples/core.md

Pattern 2: Derived Atoms

Dependencies are whatever the read function called get on, re-derived on each evaluation and cached until one of them changes.

const subtotalAtom = atom((get) => get(priceAtom) * get(quantityAtom));
const taxAtom = atom((get) => get(subtotalAtom) * get(taxRateAtom));
const totalAtom = atom((get) => get(subtotalAtom) + get(taxAtom));

Full code: examples/core.md

Pattern 3: Write-Only Atoms

A null read function marks an atom that only performs updates. Every set inside one write lands together.

const resetAllAtom = atom(null, (get, set) => {
  set(countAtom, 0);
  set(itemsAtom, []);
  set(selectedAtom, null);
});

Full code: examples/core.md

Pattern 4: Read-Write Atoms

A lens onto part of a larger value: the read narrows, the write puts the whole value back.

const nameAtom = atom(
  (get) => get(userAtom).name,
  (get, set, newName: string) => {
    set(userAtom, { ...get(userAtom), name: newName });
  },
);

Full code: examples/core.md

Pattern 5: Async Atoms with Suspense

An async read function makes the atom suspend when read.

const userAtom = atom(async (get) => {
  const id = get(userIdAtom);
  const response = await fetch(`/api/users/${id}`);
  return response.json() as Promise<User>;
});

// Or, to handle the states by hand:
const loadableUserAtom = loadable(userAtom);
// { state: "loading" } | { state: "hasData", data } | { state: "hasError", error }

Full code: examples/async.md

Pattern 6: atomWithStorage for Persistence

Backs an atom with localStorage, sessionStorage or a storage object of your own. Setting it to RESET restores the initial value.

import { atomWithStorage, RESET } from "jotai/utils";

const themeAtom = atomWithStorage<Theme>("app-theme", "light");

By default the first render shows the initial value and the stored value arrives after, which flickers. { getOnInit: true } reads storage immediately, at the cost of a server render that cannot agree with it.

Full code: examples/persistence.md

Pattern 7: splitAtom for Arrays

Turns an array atom into an atom of item atoms, so editing one item re-renders one row.

import { splitAtom } from "jotai/utils";

const todosAtom = atom<Todo[]>([]);
const todoAtomsAtom = splitAtom(todosAtom, (todo) => todo.id);

The key extractor is what keeps an item's atom identity stable across a reorder.

Full code: examples/persistence.md

Pattern 8: Stores and Providers

A store is where atom values actually live. Creating one explicitly gives access outside React, and isolation between trees.

import { createStore, Provider } from "jotai";

const store = createStore();
store.set(countAtom, 10);
store.sub(countAtom, () => {
  /* value changed */
});

<Provider store={store}>
  <App />
</Provider>;

Full code: examples/testing.md

</patterns>

<red_flags>

Red flags

Breaks at runtime:

  • An atom created inside a component — a new identity each render, so the state resets and the UI appears frozen. Where an atom genuinely has to depend on a prop, useMemo over that prop is the narrow exception.
  • An async atom read with no Suspense boundary above it — the suspension propagates as an error rather than a fallback.
  • atomFamily from jotai/utils — deprecated; jotai-family is the current package.
  • A family with no eviction — every parameter ever passed keeps an atom alive. remove() or setShouldRemove() is what bounds it.

Surprising behaviour:

  • Each Provider holds its own values, so the same atom under two Providers is two pieces of state. Passing one store to both is what shares them.
  • With no Provider at all, atoms resolve against a single global store shared by the whole app — which is why tests bleed into one another unless each gets a fresh store.
  • atomWithStorage renders the initial value first and the stored value after, so a persisted theme flashes the default. getOnInit: true fixes the flash and introduces a hydration mismatch wherever the server rendered the default.
  • loadable() never throws and never suspends; it returns a union, so reading .data without checking .state is reading a field that is not always there.
  • Grouping unrelated fields into one atom re-renders every reader on every change — the same trade a single large store makes, taken inside a library built to avoid it.
  • Atoms are compared by identity, not by contents, so two atoms created from the same initial value are two independent pieces of state.

</red_flags>

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Hugging Face Inference SDK patterns for TypeScript/Node.js — InferenceClient setup, chat completion, text generation, streaming, embeddings, image generation, audio transcription, translation, summarization, and Inference Endpoints

The English summary is being prepared. Showing the original description.

agents-inc/skills25Updated Oct 17, 2026

LiteLLM proxy server setup, TypeScript client patterns via OpenAI SDK, model routing, fallbacks, load balancing, spend tracking, virtual keys, and production deployment

The English summary is being prepared. Showing the original description.

agents-inc/skills25Updated Oct 17, 2026

Serverless GPU compute platform for AI model deployment — web endpoints, GPU functions, model serving, and TypeScript client patterns

The English summary is being prepared. Showing the original description.

agents-inc/skills25Updated Oct 17, 2026

Local LLM inference with the Ollama JavaScript client -- chat, streaming, tool calling, vision, embeddings, structured output, model management, and OpenAI-compatible endpoint

The English summary is being prepared. Showing the original description.

agents-inc/skills25Updated Oct 17, 2026

Replicate SDK patterns for TypeScript/Node.js -- client setup, predictions, streaming, webhooks, file handling, model versioning, deployments, and training

The English summary is being prepared. Showing the original description.

agents-inc/skills25Updated Oct 17, 2026

Together AI SDK patterns for TypeScript — client setup, chat completions, streaming, structured output, function calling, embeddings, image generation, fine-tuning, and OpenAI-compatible endpoints

The English summary is being prepared. Showing the original description.

agents-inc/skills25Updated Oct 17, 2026

See all skills by agents-inc

Report a problem with this skill