Jotai Atoms mit Claude Code: Praxisguide für React
Jotai atoms mit Claude Code planen: Derived, Async, SSR, Provider, Tests und sichere Prompts.
Claude Code einfach nur zu bitten, “Jotai State Management einzubauen”, ist zu unscharf. Das Ergebnis kann kompilieren, aber Atom-Größe, Server-State-Grenze, Provider-Position, SSR-Hydration und Tests werden zufällig. Sobald Filter, Formularentwürfe, asynchrone Reads und Detailseiten-UI zusammenkommen, wird diese Unschärfe teuer.
Dieser Guide behandelt Jotai atoms als Entwurfswerkzeug für kleinen React-State, nicht als generischen globalen Store. Die offizielle Jotai atom Dokumentation erklärt, dass ein atom config nur eine Definition ist und keinen Wert hält; Werte leben in einem Store. Für Async nutzen wir den Jotai async guide und Reacts <Suspense> Referenz. Für Provider und SSR sind Jotai Provider und SSR utilities die Grundlage.
Claude Code wird im offiziellen overview als Tool beschrieben, das Codebases liest, Dateien editiert und Befehle ausführt. Deshalb braucht der Prompt Scope, und sensible Dateien sollten mitpermissions.deny aus den settings docs ausgeschlossen werden. Für React-Grundlagen siehe den Claude Code React Guide, für Serverdaten den TanStack Query Guide.
Das Atom-Modell zuerst klären
Ein Atom ist nicht der Wert selbst. Es ist eine stabile Definition, die beschreibt, wie Jotai einen Wert in einem Store liest oder schreibt. Ohne diese Vorgabe erstellt Claude Code gern ein großespageStateAtom mit Suchtext, API-Antwort, selektierten Zeilen, Toasts und Formularentwurf. Zentral wirkt das kurz bequem, aber jede Änderung hat zu viel Reichweite.
Vor dem Code sollten drei Fragen beantwortet sein. Ist der Wert UI-State oder Server-Wahrheit? Wird er von weit entfernten Komponenten geteilt oder ist er lokal? Kann er aus anderen Atoms berechnet werden? Suchtext, Filter, aktiver Tab, kurzer Toast und Multi-Step-Formularentwurf passen gut. Produktlisten, User-Tabellen, Tokens, Payment-Sessions und echter Bestand gehören meist in Server-State oder auf den Server.
| Zustand | Gute Jotai-Nutzung | Vermeiden |
|---|---|---|
| Formularentwurf | Zwischen Schritten geteilt | Vollständige gespeicherte Bestellung |
| Admin-Filter | Tabelle, Count und URL lesen ihn | Ganze API-Antwort speichern |
| Modal und Toast | Von entfernten Buttons ausgelöst | Lange Logs oder Audit-Daten |
| Präferenz | Theme, Dichte, eingeklappte Hinweise | Token, E-Mail, Adresse, Payment |
Masas Fehler in einer Admin-Oberfläche war, “Jotai nutzen” zum Ziel zu machen. Die erste Version speicherte Filter, geladene Zeilen, Auswahl, Saving-State und Toasts zusammen. Nach Navigation zurück beeinflusste alte Auswahl eine neue Bulk-Aktion. Die Trennung von Serverdaten, Auswahl und kurzlebiger UI verkleinerte den Claude-Code-Diff und machte das Review klarer.
Installation und minimaler Slice
In einer Vite- oder Next.js-React-App installierst du Jotai zuerst. Wenn neue Atom Families gebraucht werden, nutzejotai-family, da die aktuellen Jotai-DokumenteatomFamily ausjotai/utils für Jotai v3 als deprecated markieren.
npm i jotai jotai-family
npm i -D vitest @testing-library/react @testing-library/user-event
Dieses TaskBoard ist klein, aber vollständig. Es enthält primitive atoms, derived atoms und write-only atoms in einem kopierbaren React-Beispiel.
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
export type TaskStatus = "todo" | "doing" | "done";
export type Task = {
id: string;
title: string;
status: TaskStatus;
};
const createId = () =>
globalThis.crypto?.randomUUID?.() ?? String(Date.now());
export const tasksAtom = atom<Task[]>([
{ id: "task-1", title: "Write release note", status: "todo" },
]);
export const filterAtom = atom<TaskStatus | "all">("all");
export const draftTitleAtom = atom("");
export const visibleTasksAtom = atom((get) => {
const filter = get(filterAtom);
const tasks = get(tasksAtom);
return filter === "all"
? tasks
: tasks.filter((task) => task.status === filter);
});
export const taskStatsAtom = atom((get) => {
const tasks = get(tasksAtom);
return {
total: tasks.length,
done: tasks.filter((task) => task.status === "done").length,
};
});
export const addTaskAtom = atom(null, (get, set) => {
const title = get(draftTitleAtom).trim();
if (!title) return;
set(tasksAtom, (tasks) => [
...tasks,
{ id: createId(), title, status: "todo" },
]);
set(draftTitleAtom, "");
});
export const toggleTaskAtom = atom(null, (_get, set, id: string) => {
set(tasksAtom, (tasks) =>
tasks.map((task) =>
task.id === id
? { ...task, status: task.status === "done" ? "todo" : "done" }
: task,
),
);
});
export function TaskBoard() {
const [draft, setDraft] = useAtom(draftTitleAtom);
const [filter, setFilter] = useAtom(filterAtom);
const tasks = useAtomValue(visibleTasksAtom);
const stats = useAtomValue(taskStatsAtom);
const addTask = useSetAtom(addTaskAtom);
const toggleTask = useSetAtom(toggleTaskAtom);
return (
<section>
<p>
Total: {stats.total} / Done: {stats.done}
</p>
<label>
New task
<input
value={draft}
onChange={(event) => setDraft(event.currentTarget.value)}
/>
</label>
<button type="button" onClick={addTask}>
Add
</button>
<select
value={filter}
onChange={(event) =>
setFilter(event.currentTarget.value as TaskStatus | "all")
}
>
<option value="all">All</option>
<option value="todo">Todo</option>
<option value="doing">Doing</option>
<option value="done">Done</option>
</select>
<ul>
{tasks.map((task) => (
<li key={task.id}>
<span>{task.title}</span>
<button
type="button"
aria-label={`Mark ${task.title} done`}
onClick={() => toggleTask(task.id)}
>
{task.status === "done" ? "Undo" : "Done"}
</button>
</li>
))}
</ul>
</section>
);
}
Nach der Generierung sollte Claude Code drei Punkte prüfen: derived atoms speichern keine doppelten Werte, write-only atoms bündeln Update-Regeln, und Komponenten abonnieren nur die Atoms, die sie rendern.
Drei konkrete Use Cases
Der erste Use Case ist eine Admin-Filterleiste. search, status, page undsort können in Atoms liegen, wenn Tabelle, Count und URL-Sync sie nutzen. Die komplette API-Antwort gehört nicht hinein. Schreibe in den Prompt: “Trenne URL-sichtbare Bedingungen von reinem UI-State, API-Antworten bleiben in der Server-State-Schicht.”
Der zweite Use Case ist ein Checkout- oder Onboarding-Formular über mehrere Schritte. Draft-Felder, aktueller Schritt und Validierung passen gut. Gesendete Bestellungen, Payment-Sessions und Inventory Checks nicht. Ein write-only reset atom nach Erfolg ist sicherer als Resets in mehreren Komponenten.
Der dritte Use Case ist UI-State auf Detailseiten. Geöffnete Zeilen, aktiver Tab, selektierte ID und kurze Toast-Queue können kleine Atoms sein, damit nur betroffene Komponenten rendern. Ein einzigesdetailPageAtom ist schnell generiert, aber schwer zu testen.
Der vierte Use Case sind Präferenzen. Theme, Dichte und ausgeblendete Hinweise können persistiert werden. Private Daten gehören nicht in Browser-Storage. Auf monetarisierten Seiten ist CTA-Open-State risikoarm, Käufer-E-Mails und Coupon-Historie bleiben serverseitig.
Derived und write-only Atoms
Derived atoms berechnen Werte aus anderen Atoms. Summen, gefilterte Listen und Validierungsergebnisse sollten berechnet, nicht gespeichert werden. Write-only atoms zentralisieren Aktionen wie patch, reset oder Normalisierung vor dem Speichern.
import { atom } from "jotai";
export type CheckoutDraft = {
email: string;
postalCode: string;
agreed: boolean;
};
const emptyCheckoutDraft: CheckoutDraft = {
email: "",
postalCode: "",
agreed: false,
};
export const checkoutDraftAtom = atom<CheckoutDraft>(emptyCheckoutDraft);
export const checkoutErrorsAtom = atom((get) => {
const draft = get(checkoutDraftAtom);
const errors: Partial<Record<keyof CheckoutDraft, string>> = {};
if (!draft.email.includes("@")) {
errors.email = "Check the email address";
}
if (!/^\d{3}-?\d{4}$/.test(draft.postalCode)) {
errors.postalCode = "Enter a seven digit postal code";
}
if (!draft.agreed) {
errors.agreed = "Agreement is required";
}
return errors;
});
export const patchCheckoutDraftAtom = atom(
null,
(_get, set, patch: Partial<CheckoutDraft>) => {
set(checkoutDraftAtom, (draft) => ({ ...draft, ...patch }));
},
);
export const resetCheckoutDraftAtom = atom(null, (_get, set) => {
set(checkoutDraftAtom, emptyCheckoutDraft);
});
Der typische Fehler ist, das Ergebnis voncheckoutErrorsAtomin einem weiteren Atom zu speichern. Der Draft ändert sich, aber der Error-Snapshot bleibt alt. Die Regel für Claude Code lautet: “Speichere keine Werte, die aus aktuellen Atoms ableitbar sind.”
Async Atoms und Server-State-Grenzen
Async atoms sind hilfreich, ersetzen aber keine komplette Fetching-Strategie. Ein async read atom kann eine Promise zurückgeben, und Suspense zeigt ein fallback während des Ladens. Das passt für kleine Reads innerhalb eines UI-Bereichs.
import { Suspense } from "react";
import { atom, useAtomValue, useSetAtom } from "jotai";
type Profile = {
id: string;
name: string;
plan: "free" | "pro";
};
export const profileIdAtom = atom("masa");
export const profileAtom = atom(async (get, { signal }) => {
const id = get(profileIdAtom);
const response = await fetch(`/api/profiles/${id}`, { signal });
if (!response.ok) {
throw new Error("Failed to load profile");
}
return (await response.json()) as Profile;
});
function ProfileCard() {
const profile = useAtomValue(profileAtom);
return <p>{profile.name} is on the {profile.plan} plan.</p>;
}
function ProfileSwitcher() {
const setProfileId = useSetAtom(profileIdAtom);
return (
<button type="button" onClick={() => setProfileId("demo")}>
Load demo user
</button>
);
}
export function ProfilePanel() {
return (
<>
<ProfileSwitcher />
<Suspense fallback={<p>Loading profile...</p>}>
<ProfileCard />
</Suspense>
</>
);
}
Wenn Retry, stale time, Pagination, optimistic mutations oder Invalidation nötig sind, nutze eine Server-State-Bibliothek. Jotai darf Request-Parameter und lokalen UI-State halten; Response-Cache bleibt außerhalb.
Atom Family, SSR und Provider
Atom family ist sinnvoll, wenn jede Zeile oder jeder Tab eigenen UI-State braucht. Das Risiko ist Cache-Wachstum. Die offiziellen Docs erklären, dass Families Atoms pro Parameter halten; unbegrenzte Parameter ohne Cleanup können Memory Leaks erzeugen. Neuer Code solltejotai-family verwenden.
import { atom } from "jotai";
import { atomFamily } from "jotai-family";
type RowUi = {
expanded: boolean;
selected: boolean;
};
export const rowUiFamily = atomFamily((id: string) =>
atom<RowUi>({ expanded: false, selected: false }),
);
rowUiFamily.setShouldRemove((createdAt) => {
return Date.now() - createdAt > 10 * 60_000;
});
export const removeRowUiAtom = atom(null, (_get, _set, id: string) => {
rowUiFamily.remove(id);
});
Bei SSR ist der Provider die Grenze. Jotai funktioniert ohne expliziten Provider, aber pro Request initiale Werte, Subtree-Isolation und Tests sind mit Provider klarer. In Next.js App Router gehörtuseHydrateAtoms in eine Client-Komponente.
"use client";
import { type PropsWithChildren } from "react";
import { Provider } from "jotai";
import { useHydrateAtoms } from "jotai/utils";
import { tasksAtom, type Task } from "./TaskBoard";
type Props = PropsWithChildren<{
initialTasks: Task[];
}>;
function HydrateAtoms({ initialTasks, children }: Props) {
useHydrateAtoms(new Map([[tasksAtom, initialTasks]]));
return children;
}
export function JotaiRequestProvider(props: Props) {
return (
<Provider>
<HydrateAtoms initialTasks={props.initialTasks}>
{props.children}
</HydrateAtoms>
</Provider>
);
}
Der Fehler ist, bei wechselndem User oder Tenant denselben Store immer wieder zu hydratisieren. Standardmäßig ist Hydration pro Store als initialer Schritt gedacht. Für echten Wechsel: Provider mit neuer key remounten oder eine explizite reset action einbauen.
Tests und sichere Prompts
Der Jotai Testing Guide empfiehlt Tests aus Nutzersicht und Jotai als Implementierungsdetail. Für Claude-Code-Ausgaben heißt das: Eingabe, Klick, sichtbares Ergebnis und Reset prüfen.
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Provider } from "jotai";
import { describe, expect, it } from "vitest";
import { TaskBoard } from "./TaskBoard";
describe("TaskBoard", () => {
it("adds and completes a task", async () => {
const user = userEvent.setup();
render(
<Provider>
<TaskBoard />
</Provider>,
);
await user.type(screen.getByLabelText("New task"), "Review atoms");
await user.click(screen.getByRole("button", { name: "Add" }));
expect(screen.getByText("Review atoms").textContent).toBe("Review atoms");
await user.click(
screen.getByRole("button", { name: "Mark Review atoms done" }),
);
expect(screen.getByText(/Done: 1/).textContent).toContain("Done: 1");
});
});
Ein guter Prompt ist eng:
Lies den bestehenden React + TypeScript Screen und ordne State mit Jotai v2.
Bearbeite nur Dateien unter src/features/tasks.
Speichere keine API-Antworten in Atoms.
Nutze Atoms nur für UI-State und Formularentwürfe.
Füge derived atoms, write-only atoms, Provider boundary und Vitest Tests hinzu.
Falls atom family nötig ist, nutze jotai-family mit Cleanup.
Schließe mit Risiken zu Fehlerfällen, Rendering und SSR.
Schließe Secrets zusätzlich über Settings aus:
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)",
"Read(./build)"
]
}
}
CTA und Praxistest
Ein Jotai-Artikel sollte nicht bei Snippets enden. Leser brauchen eine Entscheidungsregel: Was gehört in atoms, was in TanStack Query, was bleibt auf dem Server. Wenn du Claude-Code-Prompts,CLAUDE.md, Review-Regeln und Testnachweise ordnen willst, starte mit derkostenlosen Claude Code Checkliste und vergleicheZustand,TanStack Query undTeststrategien.
Masa testete dieses Muster auf einem kleinen React-Screen. Der größte Gewinn kam vor dem ersten Atom: Der erste Prompt mischte API-Antwort, Formularentwurf und Toast in ein Atom. Der zweite Prompt sagte “Serverdaten bleiben draußen”, “Derived Values werden nicht gespeichert” und “Provider plus Tests sind Pflicht”. Der Diff wurde kleiner, Vitest deckte Add, Complete und Reset ab, und das Review fand zwei leicht übersehene Risiken: Atom-Family-Cleanup und SSR-Hydration-Grenzen.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Workflow von Obsidian zu CLAUDE.md
Obsidian-Arbeitsnotizen in CLAUDE.md-Betriebsnotizen verwandeln und Kontext nicht ständig neu erklären.
Claude Code Revenue CTA Routing: Artikel zu PDF, Gumroad und Beratung führen
Ein Claude-Code-Ablauf, der Leser nach Absicht zu Gratis-PDF, Gumroad oder Beratung führt.
Claude-Code-Team-Handoff-Regeln: Belege, Berechtigungen, Rollback und Umsatzpfade
Ein praktisches Claude-Code-Handoff für Review-Belege, Berechtigungen, Rollback, Gratis-PDF, Gumroad und Beratung.