
Redux Monolitinden Zustand ve TanStack Query’ye Migration: Global State’i Parçalayan 3 Aşamalı Geçiş

Giriş
2018-2021 dönemi arasında inşa edilen birçok React uygulamasında temel bir mimari hata yapıldı: İstemci tarafındaki her türlü veriyi (API yanıtları, UI durumları, form verileri) tek bir Redux monolitinin içine doldurmak. Production ortamında 150’den fazla reducer, 4.2MB main bundle ve basit bir modal açılışında bile 120ms süren re-render döngüleri ile karşı karşıya kaldığınızda, sorunun kütüphanede değil, state yönetim paradigmasında olduğunu anlıyorsunuz.
Bir e-ticaret platformunun ödeme sayfasında, kullanıcının sepet verisi (Server State) ile sepetin açık kalma durumu (Client State) aynı global store’da tutulduğunda, sepet verisini güncellemek tüm UI bileşenlerini render döngüsüne sokar. Bu mimari çıkmazı çözmek için global state’i ikiye bölüyoruz: Asenkron, önbelleklenmesi gereken ve sunucuya ait veriler için TanStack Query v5; senkron, anlık ve istemciye ait veriler için Zustand v4. Bu ayrım sayesinde p99 render gecikmelerimizi 800ms’den 120ms’ye, JavaScript payload’umuzu ise %32 oranında düşürmeyi başardık.
İçindekiler
- Mevcut Durum: Redux Monolitinin Maliyeti
- Hedef Durum ve Trade-off Analizi
- 3 Aşamalı Migration Planı
- Aşama 1: İzolasyon ve TanStack Query Entegrasyonu
- Aşama 2: Zustand ile UI State’in Parçalanması
- Aşama 3: Redux’ın Fişini Çekme ve Temizlik
- Geri Dönüş Stratejisi (Rollback) ve Risk Noktaları
- Pratik Öneriler / Production Notları
- Sık Sorulan Sorular
- Sonuç
Mevcut Durum: Redux Monolitinin Maliyeti
Geleneksel bir Redux kurulumunda, basit bir kullanıcı profilini çekmek için Action Types, Action Creators, Reducers ve Thunks (veya Sagas) yazmanız gerekir. Redux Toolkit (RTK) bu boilerplate miktarını %40 oranında azaltsa da, temel problemi çözmez: Server State yönetimi.
Redux, doğası gereği istemci tabanlı senkron bir state yöneticisidir. Bir API’den gelen veriyi Redux’a koyduğunuz an, o verinin bayatlama süresini (stale time), arka planda güncellenmesini (background fetching) ve çöp toplayıcı (garbage collection) süreçlerini manuel olarak yönetmek zorundasınız. Mevcut sistemimizde usersSlice içinde isLoading, isError, data gibi flag’leri manuel yönetmek, state mantığımızın %70’ini oluşturuyordu.
| Metrik | Redux Monoliti (Mevcut) | İzole State (Hedef) |
|---|---|---|
| Main Bundle (Gzipped) | 185 KB | 143 KB (Redux silindiğinde) |
| Boilerplate Kod Satırı (1 Entity) | ~120 Satır (Slice + Thunk) | ~15 Satır (useQuery) |
| p99 Interaction to Next Paint | 340 ms | 85 ms |
| Cache Invalidation | Manuel (Zor) | Otomatik (Query Key tabanlı) |
Hedef Durum ve Trade-off Analizi
Hedefimiz, state türlerini tamamen izole etmek. API’den gelen her şey TanStack Query’nin in-memory cache’inde yaşayacak. Kullanıcının tema tercihi, açık olan dropdown’lar veya çok adımlı wizard form verileri Zustand üzerinde tutulacak.
Trade-off: Redux Toolkit Query (RTK Query) yerine neden TanStack Query?
Eğer hali hazırda devasa bir Redux altyapınız varsa, RTK Query’ye geçiş ilk bakışta en mantıklı yol gibi görünür. RTK Query, Redux store’u üzerinde çalışır. Ancak RTK Query’yi seçmek, uygulamayı sonsuza dek Redux ekosistemine bağlamak anlamına gelir. TanStack Query ise framework-agnostic bir yapıya sahiptir (React, Vue, Solid). Ayrıca TanStack Query’nin çöp toplama (garbage collection) algoritması ve Infinite Queries yönetimi, RTK Query’ye kıyasla daha esnek konfigürasyon imkanı sunar.
Trade-off: Neden Context API veya Jotai değil de Zustand?
Context API, bir state yönetim aracı değil, Dependency Injection aracıdır. Context içindeki bir değer değiştiğinde, o Context’i tüketen tüm bileşenler re-render olur. Bunu önlemek için karmaşık useMemo mimarileri kurmanız gerekir.
Jotai ise atomik bir state yöneticisidir (bottom-up yaklaşımı). Redux’tan (top-down, slice tabanlı) gelen bir ekip için Jotai’nin zihinsel modeline (mental model) geçiş zorlayıcıdır. Zustand, tıpkı Redux gibi dışarıdan erişilebilir (outside-React) store’lar yaratmanıza olanak tanır ve Redux DevTools ile doğrudan entegredir.
3 Aşamalı Migration Planı
Production ortamında, saatte 40.000 aktif kullanıcısı olan bir sistemi tek gecede yeniden yazamazsınız. Bu geçiş, Strangler Fig (Boğucu İncir) paterni kullanılarak, sistemi kesintiye uğratmadan yapılmalıdır.
Aşama 1: İzolasyon ve TanStack Query Entegrasyonu
İlk aşamada Redux’a hiç dokunmuyoruz. Amacımız sadece yeni geliştirilen özellikleri ve en kritik okuma (read) işlemlerini TanStack Query’ye kaydırmak. İki state yöneticisi (Redux ve TanStack Query) bir süre yan yana (Co-existence) yaşayacak.
// src/queries/useUserProfile.ts
import { useQuery } from '@tanstack/react-query';
import { apiClient } from '@/lib/apiClient';
export const useUserProfile = (userId: string) => {
return useQuery({
queryKey: ['users', 'profile', userId],
queryFn: async () => {
const { data } = await apiClient.get(`/v1/users/${userId}`);
return data;
},
// Stale Time: Verinin ne kadar süre 'taze' kabul edileceği (Örn: 30 saniye)
staleTime: 1000 * 30,
// GC Time: Kullanılmayan verinin cache'ten ne zaman silineceği (Örn: 10 dakika)
gcTime: 1000 * 60 * 10,
retry: 2,
});
};
Bu aşamada, bileşen içindeki Redux selector’larını yorum satırına alıp hook’a geçiyoruz:
// Öncesi:
// const { data: user, isLoading } = useSelector(state => state.users.profile);
// useEffect(() => { dispatch(fetchUser(userId)) }, [userId]);
// Sonrası:
const { data: user, isLoading, isError } = useUserProfile(userId);
Aşama 2: Zustand ile UI State’in Parçalanması
Redux store’unuzda muhtemelen uiSlice, modalSlice veya themeSlice gibi API’den bağımsız parçalar vardır. Bunları Zustand’a taşıyoruz. Zustand’ın en büyük avantajı, bileşen dışında da (örneğin bir Axios interceptor içinde) kullanılabilmesidir.
// src/stores/useUIStore.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
interface UIState {
sidebarOpen: boolean;
activeModal: string | null;
toggleSidebar: () => void;
openModal: (modalId: string) => void;
closeModal: () => void;
}
export const useUIStore = create()(
devtools(
persist(
(set) => ({
sidebarOpen: false,
activeModal: null,
toggleSidebar: () =>
set((state) => ({ sidebarOpen: !state.sidebarOpen }), false, 'ui/toggleSidebar'),
openModal: (modalId) =>
set({ activeModal: modalId }, false, 'ui/openModal'),
closeModal: () =>
set({ activeModal: null }, false, 'ui/closeModal'),
}),
{
name: 'ui-storage',
// Sadece sidebar durumunu localStorage'da tut
partialize: (state) => ({ sidebarOpen: state.sidebarOpen })
}
),
{ name: 'UIStore' }
)
);
Aşama 3: Redux’ın Fişini Çekme ve Temizlik
Tüm endpoint’ler TanStack Query’ye, tüm senkron UI state’ler Zustand’a ve kompleks formlar React Hook Form’a taşındıktan sonra, projedeki son useSelector ve useDispatch kullanımlarını tespit ediyoruz. @reduxjs/toolkit ve react-redux bağımlılıklarını package.json‘dan sildiğimizde, bundle analizimizde tek bir commit ile ~42KB (gzipped) tasarruf elde ediyoruz.
Geri Dönüş Stratejisi (Rollback) ve Risk Noktaları
Büyük çaplı migration süreçlerinde big bang yaklaşımından (her şeyi tek PR’da birleştirmek) kaçınmalısınız. Aşama 1 ve 2 sırasında, verileri TanStack Query ile çekerken, onSuccess callback’leri üzerinden legacy sistemler için Redux store’u beslemeye devam edebilirsiniz (Dual-write Strategy).
Önemli Risk: Race Conditions (Yarış Durumları)
Redux thunk’ları iptal etmek zordur, ancak TanStack Query AbortController ile native olarak entegredir. Geçiş sırasında aynı bileşen hem Redux action’ı tetikleyip hem de TanStack Query çağrısı yaparsa, bileşen unmount olduğunda Redux state’inde memory leak oluşabilir. Bu yüzden her bileşen/sayfa için net bir source of truth (gerçekliğin kaynağı) tanımlanmalıdır.
Fallback Kodu Örneği (Sadece Geçiş Sürecinde Kullanılmalı):
export const useLegacyUserSync = (userId: string) => {
const dispatch = useDispatch();
return useQuery({
queryKey: ['users', userId],
queryFn: () => fetchUser(userId),
onSuccess: (data) => {
// Legacy bileşenler kırılmasın diye Redux'ı manuel besliyoruz
dispatch({ type: 'users/fetchSuccess', payload: data });
}
});
};
Pratik Öneriler / Production Notları
- Global QueryClient Konfigürasyonu:
staleTimedeğerini asla global olarak 0 bırakmayın (varsayılan değer 0’dır). Uygulamanızın türüne göre global bir minimum değer (örn: 20 saniye) belirleyin. Aksi takdirde window focus olaylarında gereksiz ağ trafiği yaratırsınız. - Zustand Slices Paternini Kullanın: Store’unuz büyüdükçe tek bir dosyada 500 satır Zustand kodu yazmayın. Zustand’ın Slice pattern dokümantasyonunu uygulayarak mantıksal domain’leri dosyalara bölün.
- Pre-fetching Stratejisi: Kullanıcı bir butona hover olduğunda veya bir sayfa route değişimine başladığında
queryClient.prefetchQueryçağırarak algılanan performansı (perceived performance) sıfır gecikme noktasına yaklaştırın. - Zustand Selector Hatası: Zustand’ı kullanırken nesne yıkımını (destructuring) doğrudan yapmayın.
const { isOpen } = useUIStore()kullanımı, store’daki herhangi bir değer değiştiğinde bileşeni re-render eder. Doğru kullanım:const isOpen = useUIStore(state => state.isOpen).
Sık Sorulan Sorular
İki state kütüphanesi (Zustand + TanStack Query) kullanmak bundle boyutunu artırmaz mı?
Hayır. Redux Toolkit ve React-Redux ikilisi gzipped olarak yaklaşık 14KB yer kaplar. TanStack Query (~13KB) ve Zustand (~1KB) toplamda aynı boyuta denk gelir. Ancak, yazdığınız boilerplate kod ve reducer/thunk dosyalarının elimine edilmesiyle, kendi yazdığınız JavaScript kodundan %30’a varan oranda tasarruf edersiniz.
Zustand’da Redux Middleware gibi yapıları kurabilir miyim?
Evet. Zustand, kendi middleware ekosistemini barındırır. devtools, persist, subscribeWithSelector gibi built-in middleware’ler ile state değişikliklerini dinleyebilir, local storage’a otomatik senkronizasyon yapabilir ve Redux DevTools eklentisini sorunsuz kullanmaya devam edebilirsiniz.
Bir sorgunun sonucu başka bir sorguya parametre oluyorsa TanStack Query ile bunu nasıl yönetiriz?
Dependent Queries (Bağımlı Sorgular) özelliği tam olarak bunun için var. İkinci useQuery hook’unun enabled parametresine, ilk sorgunun sonucunun var olup olmadığını (!!firstQueryData?.id) boolean olarak vererek ardışık sorgu zincirleri kurabilirsiniz.
Redux tabanlı testlerimi (Jest / RTL) nasıl güncelleyeceğim?
Testlerdeki en büyük değişiklik, Provider yapısında olacak. Redux’ın Provider’ını ve mock store’unu silip, yerine her test izolasyonu için sıfırdan bir QueryClient örneği yaratan bir Wrapper yazacaksınız. API çağrılarını mock’lamak için MSW (Mock Service Worker) kullanmak, TanStack Query mimarisinde endüstri standardıdır.
Sonuç
Redux’tan Zustand ve TanStack Query mimarisine geçiş, sadece kullandığınız kütüphaneleri değiştirmek değil, veriyi ele alış biçiminizi sunucu tarafı ve istemci tarafı olarak yeniden yapılandırmaktır. Bu izolasyon, frontend uygulamanızın gereksiz re-render döngülerinden kurtulmasını, asenkron hataların daha merkezi yönetilmesini ve uygulamanın uzun vadeli bakım maliyetinin düşmesini sağlar.
Migration’a başlamak için ilk aksiyon adımınız: Projenizdeki en bağımsız ve sadece okuma (GET) yapan API çağrısını seçin. Onu Redux flow’undan çıkartıp Aşama 1’deki gibi izole bir TanStack Query hook’una dönüştürerek işe başlayın. Metrikleri ölçün, ekibinizin yeni zihinsel modele adaptasyonunu gözlemleyin ve ardından diğer domainlere aşamalı olarak geçiş yapın.
Bunları da beğenebilirsiniz

PHP ile Yazıların Uzunluğunu Kısaltma Fonksiyonu
Merhabalar, bu içeriğimizde php ile yazılarımızı istediğimiz uzunluğa kolayca getirebileceğimiz kısaltma fonksiyonuna bakacağız. Fonksiyonumuzda Türkçe karakterlere uyumluluk sorunu göstermeyecek olan mb_substr() fonksiyonunu kullanıyoruz. function shortly($par,…

