Claude Code के साथ Jotai atoms डिजाइन करने की गाइड
Claude Code से Jotai atoms सुरक्षित बनाएं: derived, async, SSR, Provider, tests और prompts.
Claude Code से सिर्फ “Jotai state management जोड़ दो” कहना पर्याप्त नहीं है। ऐसा कहने पर कोड चल सकता है, लेकिन atom की सीमा, server state से दूरी, Provider कहाँ लगेगा, SSR में initial value कैसे आएगी, और test किस बात को साबित करेगा, ये सब संयोग पर छोड़ दिए जाते हैं। बाद में जब filter, form draft, async read और detail page UI एक साथ आते हैं, तब debugging मुश्किल हो जाती है।
इस लेख में Jotai atoms को React के छोटे state के design tool के रूप में देखते हैं, किसी बड़े global store के रूप में नहीं। आधिकारिक Jotai atom docs बताती हैं कि atom config सिर्फ definition है, value खुद उसमें नहीं रहती; values store में रहती हैं। async उदाहरण Jotai async guide और React की <Suspense> reference पर आधारित हैं। Provider और SSR के लिए Jotai Provider और SSR utilities को आधार माना गया है।
Claude Code की आधिकारिक overview बताती है कि यह codebase पढ़ सकता है, files edit कर सकता है और commands चला सकता है। इसलिए prompt में scope, allowed files और tests साफ लिखना जरूरी है। secrets के लिए settings docs में दियाpermissions.deny उपयोग करें। React की broader तैयारी के लिएClaude Code React guide, और server data boundary के लिएTanStack Query guide देखें।
पहले atom का mental model तय करें
atom value नहीं है। atom एक stable definition है जो Jotai को बताती है कि store में value कैसे read या write करनी है। अगर यह बात Claude Code को नहीं बताई जाती, तो वह अक्सर एक बड़ाpageStateAtom बना देता है जिसमें search text, API response, selected rows, toast और form draft सब आ जाते हैं। ऐसा code पहले छोटा दिखता है, लेकिन हर change का असर बहुत बड़े हिस्से पर पड़ता है।
कोड से पहले तीन सवाल पूछें। क्या यह UI state है या server truth? क्या इसे दूर-दूर के components share करते हैं या यह local component state होना चाहिए? क्या यह दूसरे atoms से derive हो सकता है? Search text, filters, active tab, short toast और multi-step form draft Jotai में अच्छे fit हैं। Product list, user table, auth token, payment session और inventory truth server state layer या server में रहने चाहिए।
| State | Jotai में अच्छा उपयोग | बचना चाहिए |
|---|---|---|
| Form draft | कई steps में temporary input share करना | पूरा saved order store करना |
| Admin filters | table, count और URL sync के लिए | पूरा API response atom में रखना |
| Modal/toast | दूर के button से open करना | लंबे logs या audit data |
| User preference | theme, density, dismissed hint | token, email, address, payment data |
Masa ने admin screen में गलती की थी: लक्ष्य “Jotai use करना” था, state inventory बनाना नहीं। पहली version में filters, fetched rows, selected rows, saving state और toast एक atom में थे। list पर लौटने के बाद पुरानी selection नई bulk action में लग गई। API data, selection state और short UI state अलग करने के बाद Claude Code का diff छोटा हुआ और review साफ हुआ।
Install करें और छोटा working slice बनाएं
Vite या Next.js React app में पहले Jotai install करें। नए project में atom family चाहिए तोjotai-family बेहतर default है, क्योंकि Jotai docs मेंjotai/utils काatomFamily Jotai v3 के लिए deprecated बताया गया है।
npm i jotai jotai-family
npm i -D vitest @testing-library/react @testing-library/user-event
यह TaskBoard छोटा है, पर complete है। इसमें primitive atom, derived atom और write-only atom एक साथ दिखते हैं।
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>
);
}
Claude Code से generate कराने के बाद तीन चीजें जरूर review कराएं: derived atoms duplicate values store नहीं कर रहे, write-only atoms update rules को एक जगह रख रहे हैं, और components सिर्फ वही atoms subscribe कर रहे हैं जिन्हें वे render करते हैं।
कम से कम तीन real use cases
पहला use case admin filter bar है। search, status, page औरsort atoms में हो सकते हैं, क्योंकि table, count और URL sync इन्हें use करते हैं। API response को उसी atom में न रखें। Prompt में लिखें: “URL-visible conditions और UI-only state अलग रखें, API responses server-state layer में रहें।”
दूसरा use case multi-step checkout या onboarding form है। Draft fields, current step और validation Jotai में ठीक हैं। Submitted order, payment session और inventory check Jotai में नहीं जाने चाहिए। Success के बाद एक write-only reset atom रखना, कई components में manual reset से बेहतर है।
तीसरा use case detail page UI है। Expanded rows, active tab, selected ID और short toast queue को छोटे atoms में बांटने से सिर्फ relevant components rerender होते हैं। एक बड़ाdetailPageAtom जल्दी बनता है, पर review और performance tuning कठिन हो जाते हैं।
चौथा use case user preference है। Theme, density और dismissed hint storage helper से persist हो सकते हैं। Private data browser storage में नहीं जाना चाहिए। Monetized page में CTA open state low-risk है, लेकिन buyer email और coupon history server पर रहें।
Derived atoms और write-only atoms
Derived atom दूसरे atoms से value calculate करता है। Count, filtered list और validation errors generally store नहीं किए जाते, calculate किए जाते हैं। Write-only atom patch, reset और submit से पहले normalization जैसे actions को एक जगह रखता है।
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);
});
Failure example यह है किcheckoutErrorsAtomका result किसी और atom में save कर दिया जाए। Draft बदलता है, लेकिन error snapshot पुराना रह जाता है। Claude Code को साफ कहें: “जो value current atoms से derive हो सकती है, उसे store मत करें।”
Async atom और server state boundary
Async atoms उपयोगी हैं, लेकिन वे हर data fetching rule का replacement नहीं हैं। Jotai async read atom Promise return कर सकता है, और Suspense loading fallback दिखा सकता है। यह छोटे UI area की छोटी read के लिए ठीक है।
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>
</>
);
}
अगर retry, stale time, pagination, optimistic mutation या invalidate चाहिए, तो server-state library उपयोग करें। Jotai request parameters और local UI state रख सकता है, लेकिन response cache atoms के बाहर रहे।
Atom family, SSR और Provider pitfalls
हर row या tab के लिए अलग UI state चाहिए तो atom family काम आता है। लेकिन family cache बढ़ सकता है। Official docs बताती हैं कि family parameters के हिसाब से atoms रखती है; unlimited params cleanup के बिना memory leak बन सकते हैं। नए code मेंjotai-family इस्तेमाल करें।
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);
});
SSR में Provider boundary महत्वपूर्ण है। Jotai explicit Provider के बिना भी चल सकता है, लेकिन request-specific initial values, subtree isolation और tests के लिए Provider साफ रहता है। Next.js App Router मेंuseHydrateAtoms client component में होना चाहिए।
"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>
);
}
गलती यह होती है कि user या tenant बदलने पर उसी store को बार-बार hydrate करने की कोशिश की जाए। Default flow में hydration एक initial step है। Real switching के लिए Provider को नई key से remount करें या explicit reset action दें।
Tests और safe Claude Code prompt
Jotai testing guide सलाह देती है कि user interaction की तरह tests लिखें और Jotai को implementation detail मानें। Claude Code output को approve करने से पहले input, click, visible result और reset behavior test करें।
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");
});
});
Prompt को narrow रखें:
Existing React + TypeScript screen पढ़ें और Jotai v2 से state reorganize करें।
सिर्फ src/features/tasks के files edit करें।
API responses atoms में store न करें।
Atoms केवल UI state और form drafts के लिए उपयोग करें।
Derived atoms, write-only atoms, Provider boundary और Vitest tests जोड़ें।
Atom family चाहिए तो jotai-family use करें और cleanup implement करें।
अंत में failure modes, render risk और SSR risk की critical review दें।
Secrets को settings में भी बाहर रखें:
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)",
"Read(./build)"
]
}
}
Monetization CTA और hands-on note
Jotai article सिर्फ snippets पर खत्म नहीं होना चाहिए। Reader को decision rule चाहिए: क्या atoms में जाएगा, क्या TanStack Query में जाएगा, और क्या server पर रहेगा। Claude Code prompts,CLAUDE.md, review rules और test evidence को team workflow में बदलना हो तोfree Claude Code checklist से शुरू करें। फिरZustand guide,TanStack Query guide औरtesting strategies से तुलना करें।
Masa ने इस pattern को छोटे React screen पर test किया। सबसे बड़ा सुधार atom लिखने से पहले आया। पहले prompt ने API response, form draft और toast को एक atom में मिला दिया। दूसरे prompt ने साफ लिखा: “server data atoms से बाहर”, “derived values store नहीं होंगे”, “Provider और tests जरूरी हैं।” Diff छोटा हुआ, Vitest ने add, complete और reset cover किया, और review ने दो risk पकड़े: atom family cleanup और SSR hydration boundary।
मुफ़्त PDF: Claude Code cheatsheet
Email डालें और commands, review habits तथा safe workflow वाली एक-page PDF पाएँ.
हम आपका data सुरक्षित रखते हैं और spam नहीं भेजते.
लेखक के बारे में
Masa
Claude Code workflow और team adoption पर काम करने वाला engineer.
संबंधित लेख
Claude Code Obsidian to CLAUDE.md workflow: context बार-बार न समझाएं
Obsidian notes को CLAUDE.md operating notes में बदलकर Claude Code sessions को resume करना आसान बनाएं.
Claude Code Revenue CTA Routing: article से PDF, Gumroad और consultation तक
Reader intent के आधार पर free PDF, Gumroad products और consultation तक CTA route करने वाला workflow.
Claude Code टीम हैंडऑफ नियम: review proof, permissions, rollback और revenue path
Claude Code टीम काम के लिए evidence, permission rules, rollback, free PDF, Gumroad और consultation path वाला handoff.