ज़स्टैंड स्टेट मैनेजमेंट: कॉम्प्लेक्स ऐप्स के लिए पैटर्न
रिएक्ट के अंतर्निहित राज्य प्राइमेटिव्स (useState, useReducer, useContext) स्थानीय और साझा यूआई स्थिति के लिए अच्छी तरह से काम करते हैं, लेकिन वे बड़े पैमाने पर घर्षण को प्रभावित करते हैं: संदर्भ प्रदाता प्रत्येक राज्य परिवर्तन पर पूरे पेड़ को फिर से प्रस्तुत करते हैं, useReducer को वर्बोज़ एक्शन बॉयलरप्लेट की आवश्यकता होती है, और प्रोप-ड्रिलिंग असहनीय हो जाती है। Redux इसे हल करता है लेकिन महत्वपूर्ण बॉयलरप्लेट और मानसिक ओवरहेड की कीमत पर।
ज़ुस्टैंड एक आदर्श मध्य मैदान पर है: 2.9KB, कोई प्रदाता नहीं, कोई बॉयलरप्लेट नहीं, टाइपस्क्रिप्ट-प्रथम, और रिएक्ट DevTools के साथ संगत। यह क्लाइंट-साइड एप्लिकेशन स्थिति - शॉपिंग कार्ट, साइडबार स्थिति, मल्टी-स्टेप फॉर्म डेटा, यूआई प्राथमिकताएं - में उत्कृष्टता प्राप्त करता है, जबकि टैनस्टैक क्वेरी सर्वर स्थिति को संभालती है। यह मार्गदर्शिका उत्पादन अनुप्रयोगों के लिए Zustand v5 पैटर्न को कवर करती है।
मुख्य बातें
- ज़स्टैंड केवल क्लाइंट स्थिति के लिए है; सर्वर स्थिति के लिए टैनस्टैक क्वेरी का उपयोग करें - उनकी जिम्मेदारियों को ओवरलैप न करें
- किसी प्रदाता रैपर की आवश्यकता नहीं है - स्टोर मॉड्यूल-स्तरीय सिंगलटन है, कहीं भी पहुंच योग्य है
- बड़े स्टोर के लिए स्लाइस पैटर्न का उपयोग करें: एक
create()कॉल में एकाधिक स्लाइस क्रिएटर्स लिखेंimmerमिडलवेयर क्रियाओं के अंदर प्रत्यक्ष उत्परिवर्तन सिंटैक्स को सक्षम करता है (कोई स्प्रेड ऑपरेटर नहीं)- क्रॉस-सेशन स्थिति के लिए
localStorageयाsessionStorageके साथpersistमिडलवेयरdevtoolsमिडलवेयर समय-यात्रा डिबगिंग के लिए Redux DevTools एक्सटेंशन के साथ एकीकृत होता हैuseShallowवाले चयनकर्ता उथली-समान वस्तुओं से अनावश्यक पुन: प्रस्तुतीकरण को रोकते हैं- सर्वर डेटा को कभी भी Zustand में स्टोर न करें - अमान्य होना आपकी समस्या बन जाता है
बेसिक स्टोर
// src/lib/stores/ui.store.ts
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
interface UiState {
// State
sidebarOpen: boolean;
theme: 'light' | 'dark' | 'system';
commandPaletteOpen: boolean;
activeModal: string | null;
// Actions
toggleSidebar: () => void;
setSidebarOpen: (open: boolean) => void;
setTheme: (theme: UiState['theme']) => void;
openModal: (modalId: string) => void;
closeModal: () => void;
toggleCommandPalette: () => void;
}
export const useUiStore = create<UiState>()(
devtools(
(set) => ({
sidebarOpen: true,
theme: 'system',
commandPaletteOpen: false,
activeModal: null,
toggleSidebar: () =>
set((state) => ({ sidebarOpen: !state.sidebarOpen }), false, 'toggleSidebar'),
setSidebarOpen: (open) =>
set({ sidebarOpen: open }, false, 'setSidebarOpen'),
setTheme: (theme) =>
set({ theme }, false, 'setTheme'),
openModal: (modalId) =>
set({ activeModal: modalId }, false, 'openModal'),
closeModal: () =>
set({ activeModal: null }, false, 'closeModal'),
toggleCommandPalette: () =>
set(
(state) => ({ commandPaletteOpen: !state.commandPaletteOpen }),
false,
'toggleCommandPalette'
),
}),
{ name: 'ui-store' }
)
);
// Usage in components — select only what you need
function Sidebar() {
const isOpen = useUiStore((state) => state.sidebarOpen);
const toggle = useUiStore((state) => state.toggleSidebar);
return (
<aside className={cn('sidebar', isOpen && 'sidebar--open')}>
<button onClick={toggle}>Toggle</button>
</aside>
);
}
दृढ़ता के साथ गाड़ी की दुकान
// src/lib/cart.ts
import { create } from 'zustand';
import { persist, createJSONStorage, devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
interface CartItem {
id: string;
productId: string;
name: string;
price: number;
quantity: number;
licenseType: 'one-time' | 'subscription';
}
interface CartState {
items: CartItem[];
couponCode: string | null;
couponDiscount: number;
// Computed (derived state — use selectors, not stored values)
// subtotal, total, itemCount — computed in selectors
// Actions
addItem: (item: Omit<CartItem, 'quantity'>) => void;
removeItem: (productId: string) => void;
updateQuantity: (productId: string, quantity: number) => void;
applyCoupon: (code: string, discount: number) => void;
removeCoupon: () => void;
clearCart: () => void;
}
export const useCart = create<CartState>()(
devtools(
persist(
immer((set) => ({
items: [],
couponCode: null,
couponDiscount: 0,
addItem: (newItem) =>
set((state) => {
const existing = state.items.find((i) => i.productId === newItem.productId);
if (existing) {
existing.quantity += 1; // Immer allows direct mutation
} else {
state.items.push({ ...newItem, quantity: 1 });
}
}),
removeItem: (productId) =>
set((state) => {
state.items = state.items.filter((i) => i.productId !== productId);
}),
updateQuantity: (productId, quantity) =>
set((state) => {
if (quantity <= 0) {
state.items = state.items.filter((i) => i.productId !== productId);
} else {
const item = state.items.find((i) => i.productId === productId);
if (item) item.quantity = quantity;
}
}),
applyCoupon: (code, discount) =>
set((state) => {
state.couponCode = code;
state.couponDiscount = discount;
}),
removeCoupon: () =>
set((state) => {
state.couponCode = null;
state.couponDiscount = 0;
}),
clearCart: () =>
set((state) => {
state.items = [];
state.couponCode = null;
state.couponDiscount = 0;
}),
})),
{
name: 'ecosire-cart',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
// Only persist items and coupon, not derived state
items: state.items,
couponCode: state.couponCode,
couponDiscount: state.couponDiscount,
}),
}
),
{ name: 'cart-store' }
)
);
// Selectors — computed from state, not stored
export const selectSubtotal = (state: CartState) =>
state.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
export const selectTotal = (state: CartState) => {
const subtotal = selectSubtotal(state);
return subtotal * (1 - state.couponDiscount);
};
export const selectItemCount = (state: CartState) =>
state.items.reduce((sum, item) => sum + item.quantity, 0);
बड़े स्टोर के लिए स्लाइस पैटर्न
जटिल दुकानों के लिए, स्लाइस में विभाजित करें और लिखें:
// src/lib/stores/slices/filters.slice.ts
import type { StateCreator } from 'zustand';
export interface FiltersSlice {
search: string;
status: string;
page: number;
sortField: string;
sortDirection: 'asc' | 'desc';
setSearch: (search: string) => void;
setStatus: (status: string) => void;
setPage: (page: number) => void;
setSort: (field: string, direction: 'asc' | 'desc') => void;
resetFilters: () => void;
}
const initialFilters = {
search: '',
status: 'all',
page: 1,
sortField: 'createdAt',
sortDirection: 'desc' as const,
};
export const createFiltersSlice: StateCreator<FiltersSlice> = (set) => ({
...initialFilters,
setSearch: (search) => set({ search, page: 1 }), // Reset page on search
setStatus: (status) => set({ status, page: 1 }),
setPage: (page) => set({ page }),
setSort: (field, direction) => set({ sortField: field, sortDirection: direction }),
resetFilters: () => set(initialFilters),
});
// src/lib/stores/slices/selection.slice.ts
import type { StateCreator } from 'zustand';
export interface SelectionSlice {
selectedIds: Set<string>;
selectAll: boolean;
toggleSelection: (id: string) => void;
selectAllItems: (ids: string[]) => void;
clearSelection: () => void;
}
export const createSelectionSlice: StateCreator<SelectionSlice> = (set) => ({
selectedIds: new Set(),
selectAll: false,
toggleSelection: (id) =>
set((state) => {
const next = new Set(state.selectedIds);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return { selectedIds: next, selectAll: false };
}),
selectAllItems: (ids) =>
set({ selectedIds: new Set(ids), selectAll: true }),
clearSelection: () =>
set({ selectedIds: new Set(), selectAll: false }),
});
// src/lib/stores/contacts-table.store.ts
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { createFiltersSlice, type FiltersSlice } from './slices/filters.slice';
import { createSelectionSlice, type SelectionSlice } from './slices/selection.slice';
type ContactsTableStore = FiltersSlice & SelectionSlice;
export const useContactsTableStore = create<ContactsTableStore>()(
devtools(
(...args) => ({
...createFiltersSlice(...args),
...createSelectionSlice(...args),
}),
{ name: 'contacts-table-store' }
)
);
अनावश्यक पुन: प्रस्तुतीकरण को रोकना
जब चयनित मान बदलता है (वस्तुओं के संदर्भ द्वारा) तो ज़स्टैंड घटक को फिर से प्रस्तुत करता है।
// PROBLEM: new object created on every render → infinite re-renders
const { search, page } = useContactsTableStore((state) => ({
search: state.search,
page: state.page,
}));
// SOLUTION 1: Select primitives individually
const search = useContactsTableStore((state) => state.search);
const page = useContactsTableStore((state) => state.page);
// SOLUTION 2: Use useShallow for object selections
import { useShallow } from 'zustand/react/shallow';
const { search, page } = useContactsTableStore(
useShallow((state) => ({ search: state.search, page: state.page }))
);
// SOLUTION 3: Select an action (actions never change reference)
const setSearch = useContactsTableStore((state) => state.setSearch);
जस्टैंड + टैनस्टैक क्वेरी: स्पष्ट पृथक्करण
मुख्य अनुशासन: क्लाइंट स्थिति के लिए ज़स्टैंड, सर्वर स्थिति के लिए टैनस्टैक क्वेरी - कभी भी ओवरलैप न करें।
// WRONG: storing server data in Zustand
const useContactsStore = create(() => ({
contacts: [], // Server data in Zustand — invalidation nightmare
fetchContacts: async () => { /* fetch and set */ },
}));
// CORRECT: TanStack Query for server data, Zustand for UI state
// TanStack Query
const { data: contacts } = useContacts({ page, limit, search });
// Zustand — only UI-specific state
const { search, page, setSearch, setPage } = useContactsTableStore();
क्वेरी कुंजी Zustand स्थिति परिवर्तनों पर प्रतिक्रिया करती है, इसलिए फ़िल्टरिंग/सॉर्टिंग बस काम करती है:
function ContactsPage() {
// UI state from Zustand
const search = useContactsTableStore((s) => s.search);
const page = useContactsTableStore((s) => s.page);
const status = useContactsTableStore((s) => s.status);
// Server state from TanStack Query, driven by Zustand state
const { data, isLoading } = useContacts({ search, page, status, limit: 20 });
return <ContactsTable data={data?.data ?? []} loading={isLoading} />;
}
मल्टी-स्टेप फॉर्म स्टोर
// src/lib/stores/onboarding.store.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
interface OnboardingData {
company: { name: string; size: string; industry: string };
contact: { name: string; email: string; phone: string };
requirements: { erp: string; modules: string[]; budget: string };
}
interface OnboardingStore {
currentStep: number;
completed: Set<number>;
data: Partial<OnboardingData>;
setStep: (step: number) => void;
updateStepData: <K extends keyof OnboardingData>(
step: K,
data: OnboardingData[K]
) => void;
markStepComplete: (step: number) => void;
reset: () => void;
}
export const useOnboardingStore = create<OnboardingStore>()(
persist(
(set) => ({
currentStep: 0,
completed: new Set(),
data: {},
setStep: (step) => set({ currentStep: step }),
updateStepData: (key, stepData) =>
set((state) => ({
data: { ...state.data, [key]: stepData },
})),
markStepComplete: (step) =>
set((state) => ({
completed: new Set([...state.completed, step]),
})),
reset: () => set({ currentStep: 0, completed: new Set(), data: {} }),
}),
{
name: 'onboarding-progress',
storage: createJSONStorage(() => sessionStorage),
// Serialize Set properly
partialize: (state) => ({
...state,
completed: Array.from(state.completed),
}),
merge: (persisted: any, current) => ({
...current,
...persisted,
completed: new Set(persisted.completed ?? []),
}),
}
)
);
जस्टैंड स्टोर्स का परीक्षण
// src/lib/stores/__tests__/cart.store.spec.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { useCart, selectSubtotal, selectTotal } from '../cart';
describe('Cart Store', () => {
beforeEach(() => {
// Reset store state between tests
useCart.setState({
items: [],
couponCode: null,
couponDiscount: 0,
});
});
it('adds items to the cart', () => {
const { addItem } = useCart.getState();
addItem({ id: '1', productId: 'prod_1', name: 'Odoo Module', price: 249 });
const { items } = useCart.getState();
expect(items).toHaveLength(1);
expect(items[0].quantity).toBe(1);
});
it('increments quantity when same product added again', () => {
const { addItem } = useCart.getState();
addItem({ id: '1', productId: 'prod_1', name: 'Odoo Module', price: 249 });
addItem({ id: '1', productId: 'prod_1', name: 'Odoo Module', price: 249 });
const { items } = useCart.getState();
expect(items).toHaveLength(1);
expect(items[0].quantity).toBe(2);
});
it('calculates subtotal correctly', () => {
useCart.setState({
items: [
{ id: '1', productId: 'p1', name: 'A', price: 100, quantity: 2, licenseType: 'one-time' },
{ id: '2', productId: 'p2', name: 'B', price: 50, quantity: 1, licenseType: 'one-time' },
],
});
expect(selectSubtotal(useCart.getState())).toBe(250);
});
it('applies coupon discount', () => {
useCart.setState({
items: [{ id: '1', productId: 'p1', name: 'A', price: 100, quantity: 1, licenseType: 'one-time' }],
couponCode: 'SAVE20',
couponDiscount: 0.2,
});
expect(selectTotal(useCart.getState())).toBe(80);
});
});
अक्सर पूछे जाने वाले प्रश्न
क्या मुझे अभी भी Zustand के साथ कॉन्टेक्स्ट एपीआई की आवश्यकता है?
कभी-कभार। ज़स्टैंड का मॉड्यूल-स्तरीय सिंगलटन अधिकांश कॉन्टेक्स्ट एपीआई उपयोग मामलों को प्रतिस्थापित करता है। कॉन्टेक्स्ट के लिए मुख्य शेष उपयोग मामला निर्भरता इंजेक्शन है - एक विशिष्ट स्टोर इंस्टेंस को एक सबट्री में पास करना (परीक्षण के लिए उपयोगी या जब आपको कई स्वतंत्र स्टोर इंस्टेंस की आवश्यकता होती है)। उसके लिए, Zustand createStore() + useStore() प्रदान करता है जो स्टोर इंस्टेंस के लिए रिएक्ट संदर्भ के साथ काम करता है।
ज़स्टैंड की तुलना Redux टूलकिट से कैसे की जाती है?
Redux टूलकिट में जटिल समय-यात्रा डिबगिंग के लिए अधिक संरचना (स्लाइस, रिड्यूसर, एक्शन) और बेहतर DevTools एकीकरण है। ज़स्टैंड में कम बॉयलरप्लेट, एक छोटा बंडल (Redux + RTK के लिए 2.9KB बनाम ~30KB) है, और प्रदाता की आवश्यकता नहीं है। अधिकांश अनुप्रयोगों के लिए, Zustand सरल और पर्याप्त है। यदि आपके पास एक बड़ी टीम है जो लागू पैटर्न से लाभान्वित होती है या उन्नत DevTools सुविधाओं की आवश्यकता है, तो Redux टूलकिट चुनें।
मैं Next.js ऐप राउटर SSR के साथ Zustand का उपयोग कैसे करूं?
ज़स्टैंड स्टोर मॉड्यूल-स्तरीय सिंगलटन हैं - क्लाइंट घटकों के लिए सुरक्षित हैं लेकिन सर्वर घटकों (कोई ब्राउज़र एपीआई, कोई विंडो नहीं) में उपयोग नहीं किया जा सकता है। केवल 'use client' घटकों में स्टोर प्रारंभ करें। प्रारंभिक डेटा के SSR हाइड्रेशन के लिए, सर्वर डेटा के लिए टैनस्टैक क्वेरी के HydrationBoundary का उपयोग करें। क्लाइंट-ओनली स्थिति (कार्ट, यूआई प्राथमिकताएं) के लिए जिसे नेविगेशन में जारी रखने की आवश्यकता है, localStorage के साथ persist मिडलवेयर का उपयोग करें।
immer मिडलवेयर क्या है और मुझे इसका उपयोग कब करना चाहिए?
immer मिडलवेयर ज़स्टैंड के set फ़ंक्शन को Immer के साथ लपेटता है, जिससे आप क्रियाओं में परिवर्तनशील कोड (state.items.push(item)) लिख सकते हैं जो वास्तव में हुड के नीचे अपरिवर्तनीय है। इसका उपयोग तब करें जब आपके पास नेस्टेड स्टेट अपडेट हों जो स्प्रेड सिंटैक्स के साथ अजीब हों, जैसे किसी डीप नेस्टेड प्रॉपर्टी को अपडेट करना या एरेज़ के साथ काम करना। यह आपके बंडल में ~6KB जोड़ता है - जटिल दुकानों के लिए यह इसके लायक है, सरल दुकानों के लिए अनावश्यक है।
मैं Zustand क्रियाओं में पुरानी समाप्ति से कैसे बचूँ?
Zustand वर्तमान स्थिति को set कॉलबैक में भेजता है, इसलिए किसी क्रिया के अंदर स्थिति को पढ़ते समय हमेशा कार्यात्मक फॉर्म का उपयोग करें: क्लोजर से state.count को कैप्चर करने के बजाय set((state) => ({ count: state.count + 1 }))। await के बाद स्थिति को पढ़ने वाली एसिंक क्रियाओं के लिए, प्रतीक्षा से पहले क्लोजर में स्थिति को कैप्चर करने के बजाय उपयोग के बिंदु पर getState() पर कॉल करें।
अगले चरण
ज़स्टैंड का न्यूनतम पदचिह्न और कंपोज़ेबल स्लाइस पैटर्न इसे किसी भी आकार के रिएक्ट अनुप्रयोगों में क्लाइंट स्थिति के लिए सही उपकरण बनाता है। सर्वर स्थिति के लिए टैनस्टैक क्वेरी के साथ युग्मित, आपके पास बॉयलरप्लेट ओवरहेड के बिना एक साफ, स्केलेबल राज्य प्रबंधन आर्किटेक्चर है।
ECOSIRE मानक राज्य प्रबंधन स्टैक के रूप में Zustand + TanStack Query के साथ रिएक्ट एप्लिकेशन बनाता है - जो जटिल व्यवस्थापक पैनल, ग्राहक पोर्टल और ई-कॉमर्स प्रवाह की सेवा प्रदान करता है। हमारी फ्रंटएंड इंजीनियरिंग सेवाओं का अन्वेषण करें यह जानने के लिए कि हम स्केलेबल रिएक्ट अनुप्रयोगों को कैसे आर्किटेक्ट करते हैं।
लेखक
ECOSIRE Research and Development Team
ECOSIRE में एंटरप्राइज़-ग्रेड डिजिटल उत्पाद बना रहे हैं। Odoo एकीकरण, ई-कॉमर्स ऑटोमेशन, और AI-संचालित व्यावसायिक समाधानों पर अंतर्दृष्टि साझा कर रहे हैं।
संबंधित लेख
NestJS 11 Enterprise API Patterns
Master NestJS 11 enterprise patterns: guards, interceptors, pipes, multi-tenancy, and production-ready API design for scalable backend systems.
Next.js 16 App Router: Production Patterns and Pitfalls
Production-ready Next.js 16 App Router patterns: server components, caching strategies, metadata API, error boundaries, and performance pitfalls to avoid.
API Integration Patterns for OpenClaw Agents
Technical patterns for integrating OpenClaw AI agents with REST APIs, GraphQL, webhooks, and enterprise systems. Covers authentication, error handling, rate limiting, and testing.