زوسٹینڈ اسٹیٹ مینجمنٹ: پیچیدہ ایپس کے پیٹرن
ری ایکٹ کے بلٹ ان اسٹیٹ پرائمیٹو (useState, useReducer, useContext) مقامی اور مشترکہ UI ریاست کے لیے اچھی طرح کام کرتے ہیں، لیکن وہ پیمانے پر رگڑ کو مارتے ہیں: سیاق و سباق فراہم کرنے والے ہر ریاست کی تبدیلی پر پورے درخت کو دوبارہ پیش کرتے ہیں، useReducer کو وربوز ایکشن بوائلر پلیٹ کی ضرورت ہوتی ہے، اور پروپ ڈرلنگ غیر منظم ہو جاتی ہے۔ Redux اسے حل کرتا ہے لیکن اہم بوائلر پلیٹ اور ذہنی اوور ہیڈ کی قیمت پر۔
زوسٹینڈ ایک بہترین درمیانی زمین پر قبضہ کرتا ہے: 2.9KB، کوئی فراہم کنندہ، کوئی بوائلر پلیٹ، TypeScript-first، اور React DevTools کے ساتھ مطابقت رکھتا ہے۔ یہ کلائنٹ سائڈ ایپلیکیشن اسٹیٹ — شاپنگ کارٹس، سائڈبار اسٹیٹ، ملٹی سٹیپ فارم ڈیٹا، UI ترجیحات — پر سبقت لے جاتا ہے جبکہ TanStack Query سرور اسٹیٹ کو ہینڈل کرتا ہے۔ یہ گائیڈ پروڈکشن ایپلی کیشنز کے لیے Zustand v5 پیٹرن کا احاطہ کرتا ہے۔
اہم ٹیک ویز
- زوسٹینڈ صرف کلائنٹ ریاست کے لیے ہے۔ سرور کی حالت کے لیے TanStack Query کا استعمال کریں - ان کی ذمہ داریوں کو اوورلیپ نہ کریں۔
- فراہم کنندہ کے ریپر کی ضرورت نہیں - اسٹور ماڈیول لیول سنگلٹن ہے، کہیں بھی قابل رسائی ہے۔
- بڑے اسٹورز کے لیے سلائس پیٹرن کا استعمال کریں: ایک سے زیادہ سلائس تخلیق کاروں کو ایک
create()کال میں تحریر کریںimmerمڈل ویئر ایکشن کے اندر ڈائریکٹ میوٹیشن نحو کو قابل بناتا ہے (کوئی اسپریڈ آپریٹرز نہیں)- کراس سیشن حالت کے لیے
persistمڈل ویئرlocalStorageیاsessionStorageکے ساتھdevtoolsمڈل ویئر ٹائم ٹریول ڈیبگنگ کے لیے Redux DevTools ایکسٹینشن کے ساتھ ضم ہوتا ہےuseShallowوالے منتخب کنندگان اتلی مساوی اشیاء سے غیر ضروری دوبارہ رینڈرز کو روکتے ہیں- زوسٹینڈ میں سرور ڈیٹا کو کبھی بھی ذخیرہ نہ کریں - باطل کرنا آپ کا مسئلہ بن جاتا ہے۔
بنیادی اسٹور
// 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);
Zustand + TanStack استفسار: واضح علیحدگی
کلیدی نظم و ضبط: کلائنٹ کی حالت کے لیے Zustand، سرور کی حالت کے لیے TanStack Query — کبھی اوورلیپ نہیں ہوتا ہے۔
// 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();
استفسار کلید زوسٹینڈ حالت میں تبدیلیوں پر ردعمل ظاہر کرتی ہے، لہذا فلٹرنگ/چھانٹنا صرف کام کرتا ہے:
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 کے ساتھ Context API کی ضرورت ہے؟
شاذ و نادر ہی۔ زوسٹینڈ کا ماڈیول لیول سنگلٹن زیادہ تر سیاق و سباق API استعمال کے معاملات کی جگہ لے لیتا ہے۔ سیاق و سباق کے لیے استعمال کا سب سے اہم کیس انحصاری انجیکشن ہے — ایک مخصوص اسٹور مثال کو ذیلی درخت میں منتقل کرنا (ٹیسٹ کے لیے مفید ہے یا جب آپ کو متعدد آزاد اسٹور مثالوں کی ضرورت ہو)۔ اس کے لیے، Zustand فراہم کرتا ہے createStore() + useStore() جو اسٹور مثال کے لیے ایک React سیاق و سباق کے ساتھ کام کرتا ہے۔
Zustand کا موازنہ Redux Toolkit سے کیسے ہوتا ہے؟
Redux Toolkit میں پیچیدہ ٹائم ٹریول ڈیبگنگ کے لیے زیادہ ڈھانچہ (سلائسز، کم کرنے والے، ایکشنز) اور بہتر DevTools انضمام ہے۔ Zustand میں کم بوائلر پلیٹ ہے، ایک چھوٹا بنڈل (2.9KB بمقابلہ ~30KB برائے Redux + RTK)، اور اسے فراہم کنندہ کی ضرورت نہیں ہے۔ زیادہ تر ایپلیکیشنز کے لیے، Zustand آسان اور کافی ہے۔ Redux Toolkit کا انتخاب کریں اگر آپ کے پاس ایک بڑی ٹیم ہے جو نافذ شدہ نمونوں سے فائدہ اٹھاتی ہے یا اسے جدید ترین DevTools خصوصیات کی ضرورت ہوتی ہے۔
میں Next.js App Router SSR کے ساتھ Zustand کا استعمال کیسے کروں؟
زوسٹینڈ اسٹورز ماڈیول سطح کے سنگل ٹن ہیں — کلائنٹ کے اجزاء کے لیے محفوظ ہیں لیکن سرور کے اجزاء میں استعمال نہیں کیے جا سکتے (کوئی براؤزر APIs، کوئی ونڈو نہیں)۔ صرف 'use client' اجزاء میں اسٹورز شروع کریں۔ ابتدائی ڈیٹا کی SSR ہائیڈریشن کے لیے، سرور ڈیٹا کے لیے TanStack Query's HydrationBoundary استعمال کریں۔ صرف کلائنٹ کی حالت (کارٹ، UI ترجیحات) کے لیے جسے نیویگیشن میں برقرار رہنے کی ضرورت ہے، persist مڈل ویئر کو localStorage کے ساتھ استعمال کریں۔
immer مڈل ویئر کیا ہے اور مجھے اسے کب استعمال کرنا چاہئے؟
immer مڈل ویئر زوسٹینڈ کے set فنکشن کو Immer کے ساتھ لپیٹتا ہے، جس سے آپ کو ایکشن (state.items.push(item)) میں میوٹیٹنگ کوڈ لکھنے کی اجازت ملتی ہے جو حقیقت میں ہڈ کے نیچے ناقابل تغیر ہے۔ اس کا استعمال اس وقت کریں جب آپ کے پاس نیسٹڈ اسٹیٹ اپ ڈیٹس ہوں جو اسپریڈ سنٹیکس کے ساتھ عجیب و غریب ہیں، جیسے کہ گہرائی سے نیسٹڈ پراپرٹی کو اپ ڈیٹ کرنا یا صفوں کے ساتھ کام کرنا۔ یہ آپ کے بنڈل میں ~6KB کا اضافہ کرتا ہے — پیچیدہ اسٹورز کے لیے قابل قدر، سادہ اسٹورز کے لیے غیر ضروری۔
میں Zustand ایکشنز میں باسی بندش سے کیسے بچ سکتا ہوں؟
زوسٹینڈ موجودہ حالت کو set کال بیک پر منتقل کرتا ہے، لہذا کسی عمل کے اندر حالت پڑھتے وقت ہمیشہ فنکشنل فارم کا استعمال کریں: state.count کو بند ہونے سے کیپچر کرنے کے بجائے set((state) => ({ count: state.count + 1 }))۔ async کارروائیوں کے لیے جو await کے بعد حالت کو پڑھتے ہیں، انتظار سے پہلے بند ہونے کی حالت کو کیپچر کرنے کے بجائے استعمال کے مقام پر getState() کو کال کریں۔
اگلے اقدامات
زوسٹینڈ کا کم سے کم نشان اور کمپوز ایبل سلائس پیٹرن اسے کسی بھی سائز کی ری ایکٹ ایپلی کیشنز میں کلائنٹ اسٹیٹ کے لیے صحیح ٹول بناتا ہے۔ سرور کی حالت کے لیے TanStack Query کے ساتھ جوڑا بنایا گیا، آپ کے پاس ایک صاف، قابل توسیع اسٹیٹ مینجمنٹ فن تعمیر ہے جس میں کوئی بوائلر پلیٹ اوور ہیڈ نہیں ہے۔
ECOSIRE ایک معیاری اسٹیٹ مینجمنٹ اسٹیک کے طور پر Zustand + TanStack Query کے ساتھ React ایپلی کیشنز بناتا ہے - پیچیدہ ایڈمن پینلز، کسٹمر پورٹلز، اور ای کامرس فلو پیش کرتے ہیں۔ ہماری فرنٹ اینڈ انجینئرنگ سروسز کو دریافت کریں یہ جاننے کے لیے کہ ہم کس طرح توسیع پذیر React ایپلی کیشنز کو آرکیٹیکٹ کرتے ہیں۔
تحریر
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.