Zustand Durum Yönetimi: Karmaşık Uygulamalara Yönelik Kalıplar
React'in yerleşik durum temel öğeleri (useState, useReducer, useContext) yerel ve paylaşılan kullanıcı arayüzü durumları için iyi çalışır, ancak ölçekte anlaşmazlıklara neden olurlar: bağlam sağlayıcıları her durum değişikliğinde ağacın tamamını yeniden işler, useReducer ayrıntılı eylem standartlarını gerektirir ve pervane sondajı yönetilemez hale gelir. Redux bunu çözüyor ancak önemli bir standart ve zihinsel yük pahasına.
Zustand mükemmel bir orta noktayı işgal ediyor: 2,9 KB, sağlayıcı yok, ortak metin yok, TypeScript öncelikli ve React DevTools ile uyumlu. İstemci tarafı uygulama durumunda (alışveriş sepetleri, kenar çubuğu durumu, çok adımlı form verileri, kullanıcı arayüzü tercihleri) öne çıkarken, TanStack Query sunucu durumunu yönetir. Bu kılavuz, üretim uygulamalarına yönelik Zustand v5 modellerini kapsar.
Önemli Çıkarımlar
- Zustand yalnızca istemci durumu içindir; sunucu durumu için TanStack Sorgusunu kullanın — sorumluluklarını üst üste koymayın
- Sağlayıcı sarmalayıcısına gerek yok — mağaza modül düzeyinde tekildir ve her yerden erişilebilir
- Büyük mağazalar için dilim modelini kullanın: birden fazla dilim oluşturucuyu tek bir
create()çağrısında birleştirinimmerara yazılımı, eylemler içinde doğrudan mutasyon sözdizimini etkinleştirir (yayma işleçleri yoktur)- Oturumlar arası durum için
localStorageveyasessionStorageiçerenpersistara yazılımıdevtoolsara yazılımı, zaman yolculuğunda hata ayıklama için Redux DevTools Uzantısıyla bütünleşiruseShallowiçeren seçiciler sığ-eşit nesnelerden gereksiz yeniden oluşturmaları önler- Sunucu verilerini asla Zustand'da saklamayın — geçersiz kılmak sorununuz haline gelir
Temel Mağaza
// 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>
);
}
Kalıcılığa Sahip Sepet Mağazası
// 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);
Büyük Mağazalar için Dilim Deseni
Karmaşık mağazalar için dilimlere bölün ve oluşturun:
// 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' }
)
);
Gereksiz Yeniden İşlemelerin Önlenmesi
Zustand, seçilen değer değiştiğinde bileşeni yeniden işler (nesneler için referans olarak).
// 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 Sorgusu: Ayrımı Temizle
Temel disiplin: İstemci durumu için Zustand, sunucu durumu için TanStack Query — asla örtüşmez.
// 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();
Sorgu anahtarı Zustand durumu değişikliklerine tepki verir, dolayısıyla filtreleme/sıralama işe yarar:
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} />;
}
Çok Adımlı Form Mağazası
// 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 ?? []),
}),
}
)
);
Zustand Mağazalarını Test Etme
// 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);
});
});
Sıkça Sorulan Sorular
Zustand'da hâlâ Context API'ye ihtiyacım var mı?
Nadiren. Zustand'ın modül düzeyindeki singleton'u çoğu Context API kullanım durumunun yerini alır. Bağlam için kalan ana kullanım durumu, belirli bir mağaza örneğinin bir alt ağaca aktarılması olan bağımlılık enjeksiyonudur (test için veya birden fazla bağımsız mağaza örneğine ihtiyaç duyduğunuzda faydalıdır). Bunun için Zustand, mağaza örneği için React bağlamıyla çalışan createStore() + useStore() sağlar.
Zustand, Redux Toolkit ile karşılaştırıldığında nasıldır?
Redux Araç Takımı, karmaşık zaman yolculuğu hata ayıklaması için daha fazla yapıya (dilimler, azaltıcılar, eylemler) ve daha iyi DevTools entegrasyonuna sahiptir. Zustand'da daha az ortak metin ve daha küçük bir paket bulunur (Redux + RTK için 2,9 KB'ye karşın ~30 KB) ve bir Sağlayıcı gerektirmez. Çoğu uygulama için Zustand daha basit ve yeterlidir. Zorunlu kalıplardan yararlanan veya gelişmiş DevTools özelliklerine ihtiyaç duyan büyük bir ekibiniz varsa Redux Toolkit'i seçin.
Zustand'ı Next.js App Router SSR ile nasıl kullanırım?
Zustand depoları modül düzeyinde tekildir; istemci bileşenleri için güvenlidir ancak Sunucu Bileşenlerinde kullanılamaz (tarayıcı API'leri yok, pencere yok). Yalnızca 'use client' bileşenlerindeki depoları başlatın. Başlangıç verilerinin SSR hidrasyonu için sunucu verileri için TanStack Query'nin HydrationBoundary öğesini kullanın. Gezinme boyunca devam etmesi gereken yalnızca istemci durumu (sepet, kullanıcı arayüzü tercihleri) için localStorage ile persist ara yazılımını kullanın.
immer ara yazılımı nedir ve onu ne zaman kullanmalıyım?
immer ara katman yazılımı, Zustand'ın set işlevini Immer ile sararak, gerçekte değişmez olan eylemlerde (state.items.push(item)) değişen kodlar yazmanıza olanak tanır. Derinlemesine yuvalanmış bir özelliği güncellemek veya dizilerle çalışmak gibi, yayılma sözdizimi açısından garip olan iç içe geçmiş durum güncellemeleriniz olduğunda bunu kullanın. Paketinize ~6 KB ekler; karmaşık mağazalar için buna değer, basit mağazalar için gereksizdir.
Zustand işlemlerinde eski kapanışları nasıl önleyebilirim?
Zustand mevcut durumu set geri çağrısına iletir, bu nedenle bir eylemin içindeki durumu okurken her zaman işlevsel formu kullanın: kapanıştan state.count yakalamak yerine set((state) => ({ count: state.count + 1 })). Bir await sonrasında durumu okuyan eşzamansız eylemler için, beklemeden önce kapanıştaki durumu yakalamak yerine kullanım noktasında getState() öğesini çağırın.
Sonraki Adımlar
Zustand'ın minimum kaplama alanı ve şekillendirilebilir dilim modeli, onu her boyuttaki React uygulamalarında istemci durumu için doğru araç haline getirir. Sunucu durumu için TanStack Sorgulama ile eşleştirildiğinde standart ek yük olmadan temiz, ölçeklenebilir bir durum yönetimi mimarisine sahip olursunuz.
ECOSIRE, standart durum yönetimi yığını olarak Zustand + TanStack Query ile React uygulamaları oluşturur ve karmaşık yönetici panellerine, müşteri portallarına ve e-ticaret akışlarına hizmet eder. Ölçeklenebilir React uygulamalarını nasıl tasarladığımızı öğrenmek için ön uç mühendislik hizmetlerimizi keşfedin.
Yazan
ECOSIRE Research and Development Team
ECOSIRE'da kurumsal düzeyde dijital ürünler geliştiriyor. Odoo entegrasyonları, e-ticaret otomasyonu ve yapay zeka destekli iş çözümleri hakkında içgörüler paylaşıyor.
İlgili Makaleler
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.