Claude Code के साथ Zustand state management
Claude Code से Zustand store, selector, persist, async action, tests और review prompt डिजाइन करें।
सबसे पहले state की सीमा तय करें
Zustand React के लिए हल्की state management library है। State management का मतलब हर variable को global object में डालना नहीं है। इसका मतलब है: कौन से values कई components में share होंगे, कौन से actions उन्हें बदलेंगे, और कौन सी छोटी चीजें component local state में रहेंगी। Claude Code जल्दी store बना सकता है, लेकिन अगर boundary साफ नहीं है तो search input, auth data, API response, toast और cart सब एक ही global store में चले जाते हैं।
इस लेख में Claude Code को सिर्फ code generator नहीं, state design assistant की तरह इस्तेमाल किया गया है। examples में admin dashboard filters, cart, authentication UI state, modal/toast और optimistic update हैं। आपको copy करने लायक TypeScript store, selector, persist partialization, async action, Vitest tests और Claude review prompt मिलेंगे।
Reference के लिए Zustand official introduction, persist middleware और useShallow guide देखें। Server state को अलग रखने के लिए TanStack Query guide और छोटे atomic state के लिए Jotai atoms article उपयोगी हैं।
Zustand में क्या रखें
सरल नियम है: Zustand में वही state रखें जिसे कई दूर के components इस्तेमाल करते हैं, जिसकी update logic को एक जगह test करना है, या जिसे URL से साफ व्यक्त नहीं किया जा सकता। Product list, user table और search result जैसे server data को सीधे Zustand में रखना अक्सर गलत है, क्योंकि उन्हें cache, refetch, stale time और error recovery चाहिए।
| Use case | Zustand में रखें | बाहर रखें | Claude Code को बताएं |
|---|---|---|---|
| Admin filters | keyword, status, page, pageSize | पूरा API response | URL state और UI state अलग करें |
| Cart | SKU, quantity, display price | payment session, real stock | सिर्फ low-risk fields persist करें |
| Auth UI | login dialog, checking state | token, email, address | PII और secrets न रखें |
| Modal/toast | activeModal, short toast queue | audit logs, long errors | UI को दिखने वाली छोटी info रखें |
| Optimistic update | requestId, previous snapshot | server की final truth | rollback और race rule लिखें |
Masa ने admin screen में एक गलती की थी: goal था “Zustand इस्तेमाल करना”, state boundary बनाना नहीं। पहले version में search text, URL query, fetched rows, selected rows और toast एक साथ थे। Navigation के बाद पुराने filters नई screen को प्रभावित कर रहे थे। Fix कोई नया middleware नहीं था; पहले यह table बनाना था कि global में क्या safe है।
flowchart LR
UI[React components] --> Selectors[Selectors and useShallow]
Selectors --> Store[Zustand store]
Store --> Actions[Actions]
Actions --> API[API and server state]
Store --> Persist[persist partialize]
Persist --> Storage[localStorage safe fields]
TypeScript store
यह store जानबूझकर सीमित है: admin filters, cart, auth UI, modal और toasts। बड़े project में files अलग कर सकते हैं, लेकिन पहले Claude Code से ऐसा complete slice बनवाना review को आसान बनाता है।
import { create, type StateCreator } from "zustand";
export type OrderStatus = "all" | "paid" | "refunded" | "failed";
export type AuthUiStatus = "anonymous" | "checking" | "signedIn";
export type ModalId = "invite-user" | "cart-drawer" | "delete-order" | null;
export interface AdminFilters {
keyword: string;
status: OrderStatus;
page: number;
pageSize: number;
}
export interface CartLine {
id: string;
name: string;
price: number;
quantity: number;
}
export interface AuthUi {
status: AuthUiStatus;
loginDialogOpen: boolean;
}
export interface Toast {
id: string;
kind: "success" | "error" | "info";
message: string;
}
export interface CommerceUiState {
filters: AdminFilters;
cart: CartLine[];
auth: AuthUi;
activeModal: ModalId;
toasts: Toast[];
setFilters: (patch: Partial<AdminFilters>) => void;
resetFilters: () => void;
addToCart: (line: Omit<CartLine, "quantity">) => void;
removeFromCart: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
setAuthStatus: (status: AuthUiStatus) => void;
setLoginDialogOpen: (open: boolean) => void;
openModal: (modal: Exclude<ModalId, null>) => void;
closeModal: () => void;
pushToast: (toast: Omit<Toast, "id">) => void;
dismissToast: (id: string) => void;
checkoutTotal: () => number;
}
export const initialFilters: AdminFilters = {
keyword: "",
status: "all",
page: 1,
pageSize: 25,
};
const createToastId = () =>
globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2);
export const createCommerceUiSlice: StateCreator<CommerceUiState> = (set, get) => ({
filters: initialFilters,
cart: [],
auth: { status: "anonymous", loginDialogOpen: false },
activeModal: null,
toasts: [],
setFilters: (patch) =>
set((state) => ({
filters: {
...state.filters,
...patch,
page: patch.page ?? 1,
},
})),
resetFilters: () => set({ filters: initialFilters }),
addToCart: (line) =>
set((state) => {
const current = state.cart.find((item) => item.id === line.id);
if (!current) {
return { cart: [...state.cart, { ...line, quantity: 1 }] };
}
return {
cart: state.cart.map((item) =>
item.id === line.id ? { ...item, quantity: item.quantity + 1 } : item,
),
};
}),
removeFromCart: (id) =>
set((state) => ({
cart: state.cart.filter((item) => item.id !== id),
})),
updateQuantity: (id, quantity) =>
set((state) => ({
cart: state.cart
.map((item) =>
item.id === id ? { ...item, quantity: Math.max(0, quantity) } : item,
)
.filter((item) => item.quantity > 0),
})),
setAuthStatus: (status) =>
set((state) => ({
auth: { ...state.auth, status },
})),
setLoginDialogOpen: (open) =>
set((state) => ({
auth: { ...state.auth, loginDialogOpen: open },
})),
openModal: (modal) => set({ activeModal: modal }),
closeModal: () => set({ activeModal: null }),
pushToast: (toast) =>
set((state) => ({
toasts: [...state.toasts, { ...toast, id: createToastId() }],
})),
dismissToast: (id) =>
set((state) => ({
toasts: state.toasts.filter((toast) => toast.id !== id),
})),
checkoutTotal: () =>
get().cart.reduce((total, item) => total + item.price * item.quantity, 0),
});
export const useCommerceUiStore = create<CommerceUiState>()(createCommerceUiSlice);
ध्यान दें कि store auth UI रखता है, auth secret नहीं। access token, email, address, legal name और payment session यहां नहीं होने चाहिए। Claude Code से persist मांगते समय साफ लिखें कि PII, यानी व्यक्तिगत पहचान योग्य जानकारी, कभी save नहीं करनी है।
Selector से rerender घटाएं
अगर component useCommerceUiStore() को बिना selector call करता है, तो वह पूरे store को subscribe करता है। फिर toast update भी cart badge को rerender कर सकता है। Selector store से सिर्फ जरूरत का हिस्सा निकालता है। Object लौटाते समय useShallow मदद करता है।
import { useShallow } from "zustand/react/shallow";
import {
useCommerceUiStore,
type CommerceUiState,
} from "./commerce-ui-store";
export const selectCartCount = (state: CommerceUiState) =>
state.cart.reduce((sum, item) => sum + item.quantity, 0);
export const selectCartTotal = (state: CommerceUiState) => state.checkoutTotal();
export function CartBadge() {
const count = useCommerceUiStore(selectCartCount);
return <button type="button">Cart ({count})</button>;
}
export function AdminFilterSummary() {
const { filters, setFilters, resetFilters } = useCommerceUiStore(
useShallow((state) => ({
filters: state.filters,
setFilters: state.setFilters,
resetFilters: state.resetFilters,
})),
);
return (
<form>
<input
value={filters.keyword}
placeholder="Search orders"
onChange={(event) => setFilters({ keyword: event.currentTarget.value })}
/>
<select
value={filters.status}
onChange={(event) =>
setFilters({ status: event.currentTarget.value as CommerceUiState["filters"]["status"] })
}
>
<option value="all">All</option>
<option value="paid">Paid</option>
<option value="refunded">Refunded</option>
<option value="failed">Failed</option>
</select>
<output>Page {filters.page}</output>
<button type="button" onClick={resetFilters}>
Reset
</button>
</form>
);
}
Claude Code से कहें कि हर component में selector check करे। Debug component को छोड़कर कोई भी पूरा store subscribe न करे। असली dashboard में table, sidebar, filter और notification साथ बदलते हैं, इसलिए यह rule performance बचाता है।
Persist को सीमित रखें
persist reload के बाद state बचाता है, लेकिन गलत field save करने पर privacy risk बनता है। Cart और filters ठीक हैं; modal, toast, requestId, token और PII नहीं। partialize से save होने वाले fields सीमित करें।
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import {
createCommerceUiSlice,
type CommerceUiState,
} from "./commerce-ui-store";
const noopStorage = {
getItem: () => null,
setItem: () => undefined,
removeItem: () => undefined,
};
type PersistedCommerceUiState = Pick<CommerceUiState, "filters" | "cart">;
export const usePersistedCommerceUiStore = create<CommerceUiState>()(
persist(createCommerceUiSlice, {
name: "commerce-ui-v1",
version: 1,
storage: createJSONStorage(() =>
typeof window === "undefined" ? noopStorage : window.localStorage,
),
partialize: (state): PersistedCommerceUiState => ({
filters: state.filters,
cart: state.cart,
}),
}),
);
SSR में server localStorage नहीं पढ़ सकता। Server HTML में cart 0 हो और browser restore करके 3 items दिखाए तो hydration mismatch हो सकता है। ऐसे values को mount के बाद दिखाएं या critical SSR markup से अलग रखें।
Async action और optimistic update
Optimistic update server confirmation से पहले UI बदलता है। Follow, like और low-risk cart quantity में यह अच्छा है। लेकिन पुराने request की response बाद में आकर नया state overwrite कर सकती है। इसलिए requestId और previous snapshot रखें।
import { create } from "zustand";
interface Profile {
id: string;
name: string;
isFollowing: boolean;
}
interface ProfileState {
profiles: Record<string, Profile>;
followRequestIds: Record<string, string>;
setProfile: (profile: Profile) => void;
followOptimistically: (profileId: string) => Promise<void>;
}
const removeKey = <T,>(record: Record<string, T>, key: string) => {
const { [key]: _removed, ...rest } = record;
return rest;
};
async function updateFollowOnServer(profileId: string, follow: boolean) {
const response = await fetch(`/api/profiles/${profileId}/follow`, {
method: follow ? "PUT" : "DELETE",
});
if (!response.ok) {
throw new Error(`Follow update failed: ${response.status}`);
}
}
export const useProfileStore = create<ProfileState>((set, get) => ({
profiles: {},
followRequestIds: {},
setProfile: (profile) =>
set((state) => ({
profiles: { ...state.profiles, [profile.id]: profile },
})),
followOptimistically: async (profileId) => {
const before = get().profiles[profileId];
if (!before) {
throw new Error("Profile not found");
}
const requestId = `${profileId}-${Date.now()}`;
set((state) => ({
profiles: {
...state.profiles,
[profileId]: { ...before, isFollowing: true },
},
followRequestIds: {
...state.followRequestIds,
[profileId]: requestId,
},
}));
try {
await updateFollowOnServer(profileId, true);
} catch (error) {
set((state) => {
if (state.followRequestIds[profileId] !== requestId) return state;
return {
profiles: { ...state.profiles, [profileId]: before },
followRequestIds: removeKey(state.followRequestIds, profileId),
};
});
throw error;
}
set((state) => {
if (state.followRequestIds[profileId] !== requestId) return state;
return {
followRequestIds: removeKey(state.followRequestIds, profileId),
};
});
},
}));
Claude Code को product rule दें: repeated click allowed है या नहीं, failure पर क्या rollback होगा, toast दिखेगा या नहीं, और multiple requests में कौन जीतेगा।
Vitest tests
Zustand actions को React render किए बिना test कर सकते हैं। हर test से पहले initial state restore करें।
import { beforeEach, describe, expect, it } from "vitest";
import { useCommerceUiStore } from "./commerce-ui-store";
const initialState = useCommerceUiStore.getInitialState();
beforeEach(() => {
useCommerceUiStore.setState(initialState, true);
});
describe("commerce ui store", () => {
it("increments cart quantity and calculates total", () => {
const store = useCommerceUiStore.getState();
store.addToCart({ id: "sku-1", name: "Workshop", price: 1200 });
store.addToCart({ id: "sku-1", name: "Workshop", price: 1200 });
expect(useCommerceUiStore.getState().cart[0]?.quantity).toBe(2);
expect(useCommerceUiStore.getState().checkoutTotal()).toBe(2400);
});
it("resets the page when a filter changes", () => {
useCommerceUiStore.getState().setFilters({ page: 4 });
useCommerceUiStore.getState().setFilters({ keyword: "refund" });
expect(useCommerceUiStore.getState().filters).toMatchObject({
keyword: "refund",
page: 1,
});
});
it("keeps auth UI and toasts explicit", () => {
useCommerceUiStore.getState().setAuthStatus("signedIn");
useCommerceUiStore.getState().pushToast({
kind: "success",
message: "Saved",
});
expect(useCommerceUiStore.getState().auth.status).toBe("signedIn");
expect(useCommerceUiStore.getState().toasts).toHaveLength(1);
});
});
Claude review prompt
इस diff में सिर्फ Zustand state-management design review करें।
Check:
1. Local state वाली values global store में नहीं गईं।
2. Components selectors इस्तेमाल करते हैं, पूरा store नहीं।
3. persist partialize PII, token, modal, toast, requestId save नहीं करता।
4. SSR/hydration mismatch handle है।
5. Async actions stale response, repeated click और rollback handle करते हैं।
6. devtools और immer बिना reason add नहीं हुए।
7. Vitest main actions और कम से कम एक failure path cover करता है।
Return:
- Blocking issues
- Suggested patches
- Missing tests
- Questions before merge
Pitfalls और verification
मुख्य गलतियां हैं: सब कुछ global करना, selector भूलना, PII persist करना, SSR/hydration ignore करना, async race न संभालना और devtools या immer को आदत से जोड़ना। Store decision को साफ करे, छुपाए नहीं।
Claude Code Lab कीtraining और consulting में existing React app देखकर यह तय किया जा सकता है कि कौन सा state Zustand में जाएगा, कौन सा TanStack Query में रहेगा, कौन से fields persist होंगे और team review prompt कैसा होगा। Verification: 2 जून 2026 को examples को Zustand official docs में create, persist, useShallow और testing के अनुसार check किया गया। Practical result यह है कि पहले safe persist fields की table बनाने से localStorage PII cleanup और hydration fixes कम हुए।
मुफ़्त 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.