2025'te Zustand vs Redux: Kurumsal Durum Yönetimi İçin Neden Zustand'ı Seçtik
Kurumsal mobil bankacılık uygulamamızın durum yönetimi katmanını tasarlamaya başladığımda, varsayılan tercih bariz görünüyordu: Redux. Savaşta sınanmış, devasa bir ekosistem desteğine sahip ve ekipteki her kıdemli geliştirici daha önce kullanmıştı. Ancak iki haftalık prototipleme sonrası, beni bile şaşırtan bir karar verdim — Zustand'ı seçtik. Sekiz store ve sıfır pişmanlık sonrası, nedenini anlatıyorum.
Modern React'te Redux'ın Sorunu
Yanlış anlamayın — Redux olağanüstü bir kütüphane. Ancak 2025'te, React 18+ ve eşzamanlı (concurrent) özelliklerle birlikte, Redux'ın gerektirdiği seremoni gerçek bir verimlilik vergisine dönüştü. Uygulamamızda kimlik doğrulama durumunu, kullanıcı profillerini, hesap bakiyelerini, işlem geçmişini, bildirim tercihlerini, biyometrik ayarları, tema yapılandırmasını ve WebView iletişim durumunu yönetmemiz gerekiyordu. Bu, sekiz ayrı alan (domain) demek.
Redux Toolkit ile her bir alan şunlara ihtiyaç duyardı:
typescript// Redux approach - per domain
// 1. Slice file with reducers
// 2. Async thunks for API calls
// 3. Selectors (memoized with reselect)
// 4. Types for state, actions, payloads
// 5. Registration in root store
// 6. Middleware configuration
// store/slices/authSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const loginThunk = createAsyncThunk(
'auth/login',
async (credentials: LoginCredentials, { rejectWithValue }) => {
try {
const response = await authService.login(credentials);
return response.data;
} catch (err) {
return rejectWithValue(err.response.data);
}
}
);
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: { /* ... */ },
extraReducers: (builder) => {
builder
.addCase(loginThunk.pending, (state) => { state.loading = true; })
.addCase(loginThunk.fulfilled, (state, action) => { /* ... */ })
.addCase(loginThunk.rejected, (state, action) => { /* ... */ });
},
});
Bunu sekiz alanla çarpın. Tek bir satır iş mantığı yazmadan önce, bir hayli kalıp kod (boilerplate) demek bu.
Zustand Sahnede: Fedakârlık Yapmadan Sadelik
Zustand (bizim durumumuzda 5.0.8 sürümü) temelden farklı bir yaklaşım benimser. Bir store, sadece bir hook'tur. İşte gerçek auth store kalıbımız:
typescript// stores/useAuthStore.ts
import { create } from 'zustand';
interface AuthState {
user: User | null;
token: string | null;
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
login: (credentials: LoginCredentials) => Promise<void>;
logout: () => Promise<void>;
refreshToken: () => Promise<void>;
clearError: () => void;
}
export const useAuthStore = create<AuthState>((set, get) => ({
user: null,
token: null,
isAuthenticated: false,
isLoading: false,
error: null,
login: async (credentials) => {
set({ isLoading: true, error: null });
try {
const response = await authService.login(credentials);
set({
user: response.user,
token: response.token,
isAuthenticated: true,
isLoading: false,
});
} catch (error) {
set({
error: error.message,
isLoading: false,
});
throw error;
}
},
logout: async () => {
await authService.logout(get().token);
set({
user: null,
token: null,
isAuthenticated: false,
});
},
refreshToken: async () => {
const currentToken = get().token;
if (!currentToken) return;
const newToken = await tokenManager.refresh(currentToken);
set({ token: newToken });
},
clearError: () => set({ error: null }),
}));
Hepsi bu. Tek dosya. Durum, eylemler ve asenkron işlemler hepsi bir arada. Asenkron için middleware'e ihtiyaç yok — doğrudan async fonksiyonları kullanıyorsunuz.
Middleware'i Neden Tümüyle Bıraktık
En tartışmalı kararlarımızdan biri, Zustand'ı hiçbir middleware olmadan çalıştırmaktı — ne persist, ne devtools, ne de immer. İşte nedeni:
Kalıcılık (persistence): Token kalıcılığını, devre kesici (circuit breaker) kalıbına sahip özel TokenManager'ımız üzerinden hallediyoruz. Kullanıcı tercihleri backend ile senkronize oluyor. Tam bir uygulama yeniden başlatmasından sağ çıkması gereken ve API katmanımız ile TanStack Query önbelleği tarafından zaten ele alınmayan herhangi bir durum yok.
DevTools: Geliştirme modunda sekiz store'un tamamından okuyan hafif bir hata ayıklama (debug) paneli geliştirdik:
typescript// debug/StateInspector.tsx
const StateInspector = () => {
const auth = useAuthStore();
const accounts = useAccountStore();
const transactions = useTransactionStore();
// ... other stores
if (__DEV__) {
return (
<DebugOverlay
stores={{ auth, accounts, transactions }}
onReset={() => {
useAuthStore.getState().logout();
useAccountStore.setState({ accounts: [] });
}}
/>
);
}
return null;
};
Immer: TypeScript'in katı (strict) modu ve store boyutlarımızla (hiçbiri 15 özelliği aşmıyor), spread operatörleri değişmezliği (immutability) gayet iyi hallediyor. Immer, gzip'lenmiş 12KB ekliyor — ve mobilde bu önemli.
Thunk Middleware Olmadan Async Thunk'lar
Zustand'daki asenkron kalıp olağanüstü sade. Ne createAsyncThunk, ne aksiyon tipleri, ne de ekstra reducer'lar. İşlem (transaction) store'umuz bunu güzel gösteriyor:
typescriptexport const useTransactionStore = create<TransactionState>((set, get) => ({
transactions: [],
filters: defaultFilters,
pagination: { page: 1, hasMore: true },
fetchTransactions: async (reset = false) => {
const { filters, pagination } = get();
if (!reset && !pagination.hasMore) return;
const page = reset ? 1 : pagination.page;
set({ isLoading: true });
const result = await transactionService.fetch({
...filters,
page,
limit: 20,
});
set((state) => ({
transactions: reset
? result.items
: [...state.transactions, ...result.items],
pagination: {
page: page + 1,
hasMore: result.hasMore,
},
isLoading: false,
}));
},
applyFilters: async (newFilters) => {
set({ filters: { ...get().filters, ...newFilters } });
await get().fetchTransactions(true);
},
}));
applyFilters'ın doğrudan fetchTransactions'ı çağırdığına dikkat edin. Redux'ta bir thunk içinden başka bir thunk dispatch etmeniz gerekirdi ki bu da dispatch'i bir parametre olarak almayı gerektirir. Burada, get() size ihtiyacınız olan her şeyi verir.
Store'lar Arası İletişim
Birden fazla store ile odadaki fil, store'lar arası iletişimdir. Her şey tek bir ağaçta yaşadığı için Redux bunu doğal olarak halleder. Zustand'da ise dışarıdan getState() kullanırsınız:
typescript// In useNotificationStore
markAsRead: async (notificationId: string) => {
await notificationService.markRead(notificationId);
// Update unread count in account store
const currentBadge = useAccountStore.getState().unreadNotifications;
useAccountStore.setState({
unreadNotifications: Math.max(0, currentBadge - 1),
});
set((state) => ({
notifications: state.notifications.map((n) =>
n.id === notificationId ? { ...n, read: true } : n
),
}));
},
Bu açık ve izlenebilir. useAccountStore.setState için grep yapıp her harici mutasyonu bulabilirsiniz. Redux'ta ise tüm kod tabanında aksiyon tipi dizgelerini (string) aramanız gerekirdi.
Performans: Zustand'ın Gizli Silahı
Zustand varsayılan olarak referans eşitliği (reference equality) kullanır; bu da bileşenlerin yalnızca abone oldukları belirli durum diliminde değişiklik olduğunda yeniden render edilmesi anlamına gelir:
typescript// Only re-renders when user.name changes
const userName = useAuthStore((state) => state.user?.name);
// Only re-renders when isAuthenticated changes
const isAuth = useAuthStore((state) => state.isAuthenticated);
Redux'ta bunu useSelector ile elde edersiniz, ancak türetilmiş veriler için Reselect'ten createSelector'a da ihtiyaç duyarsınız. Zustand bunu, gerektiğinde yüzeysel (shallow) karşılaştırmayla yerel olarak halleder:
typescriptimport { useShallow } from 'zustand/react/shallow';
const { user, isLoading } = useAuthStore(
useShallow((state) => ({
user: state.user,
isLoading: state.isLoading,
}))
);
Paket Boyutu ve Mobil Performans
React Native bir bankacılık uygulamasında her kilobayt önemlidir. İşte karşılaştırma:
| Kütüphane | Gzip'lenmiş Boyut | |---------|-------------| | Redux + Toolkit + React-Redux | ~11.2 KB | | Zustand 5.0.8 | ~1.1 KB |
Bu 10KB'lık fark, gelişmekte olan pazarlarda 3G bağlantılardaki gerçek bant genişliğidir — ve bankacılık uygulamamızın temel demografisidir.
Redux'ın Hâlâ Kazandığı Durumlar
Adil olmak istiyorum. Zaman yolculuğu (time-travel) hata ayıklamasına birinci sınıf bir özellik olarak ihtiyacınız olduğunda, durum ağacınız derinlemesine iç içe ve birbirine bağımlı olduğunda, Redux'ın dayattığı kalıplardan faydalanan büyük bir ekibiniz olduğunda veya middleware ekosistemine (saga, observable vb.) ihtiyaç duyduğunuzda Redux hâlâ daha iyi bir tercihtir.
Bizim kullanım senaryomuz için — sekiz odaklı store, satır içi (inline) ele alınan asenkron işlemler, middleware'e ihtiyaç olmaması — Zustand açık ara kazanandı. Kod tabanı %40 daha küçük, yeni geliştiricilerin uyumu günler yerine saatler alıyor ve durum yönetimi katmanımız altı ay sonra üretimde sıfır hatayla çalışıyor.
Sonuç
En iyi araç, kısıtlarınıza uyan araçtır. Zustand bizimkilere mükemmel uydu: TypeScript öncelikli, minimum kalıp kod, minik paket boyutu ve doğrudan React hook'larına eşlenen bir zihinsel model. 2025'te yeni bir React veya React Native projesine başlıyorsanız, Zustand'a dürüst bir değerlendirme şansı verin. Redux'ı ne kadar az özlediğinize şaşırabilirsiniz.