Yazılarımız

Veri Akademi

REACT STATE MANAGEMENT: CONTEXT VS REDUX VS ZUSTAND (SEÇİM REHBERİ)

React ekosisteminde “state yönetimi” konusu, birkaç küçük useState çağrısından devasa ürünlere uzanan bir yelpazeyi kapsar. Bir yanda Context ile sade bir paylaşım modeli, diğer yanda Redux ile disiplinli mimari, arada ise Zustand gibi hafif ama güçlü alternatifler var. Doğru seçim, “en popüler hangisi?” değil; ürünün ölçeği, ekibin çalışma biçimi ve performans beklentisiyle ilgilidir.

Bu rehber, Context vs Redux vs Zustand karşılaştırmasını yalnızca özellik listesi olarak değil, karar vermeyi kolaylaştıran bir çerçeve olarak ele alır. Hangi yaklaşımın hangi problem sınıfında parladığını, hangi noktada maliyet çıkardığını ve birlikte kullanım senaryolarını gerçekçi örneklerle göreceksiniz.

Okurken aklınızda şu soru olsun: “Benim projemde state nerede doğuyor, kim tüketiyor, ne kadar sık değişiyor ve hatayı yakalamak ne kadar kritik?” Bu sorulara net yanıtlar, doğru aracı kendiliğinden işaret eder.

Context, Redux ve Zustand arasında seçim yaparken ölçek, ekip ve bakım maliyetini dengeleyen bir karar akışı

React state management seçiminde önce problemi tanımla

“React state management” deyince çoğu zaman tek bir kavramdan bahsediyor gibi davranıyoruz; oysa sorun farklı katmanlara ayrılır: bileşen içi (local), sayfa/feature içi (shared), uygulama geneli (global), sunucu kaynaklı (server state) ve cache. Bu katmanların her biri farklı bir maliyet profiline sahiptir.

Örneğin form state genellikle local veya feature içinde kalır; burada en iyi araç çoğu zaman form kütüphanesi ve basit hook’lardır. Buna karşın kimlik bilgisi, yetki, tema, dil gibi uygulama genelini etkileyen veriler bir paylaşım mekanizması ister. Sunucudan gelen veri ise (listeleme, arama, pagination) çoğunlukla ayrı bir “server state” alanıdır ve TanStack Query gibi çözümlerle daha iyi yönetilir.

Bir diğer kırılım da “ephemeral UI state” ve “kalıcı domain state” ayrımıdır. Modal açık/kapalı, tooltip görünür mü gibi kısa ömürlü durumlar, çoğunlukla bileşene yakın tutulduğunda daha anlaşılır olur. Buna karşılık sepet, yetkilendirme, filtreler, çok adımlı akış gibi domain state’ler ortak bir kaynakta yönetildiğinde tutarlılık artar.

İki kritik sinyal: fan-out ve değişim frekansı

Seçimi hızlandıran iki pratik metrik vardır. Birincisi fan-out: aynı state’i kaç farklı bileşen tüketiyor? İkincisi değişim frekansı: bu state saniyede/etkileşimde kaç kez güncelleniyor? Fan-out arttıkça, izlenebilirlik ve tutarlılık ihtiyacı yükselir. Değişim frekansı arttıkça, render maliyetini kontrol etmek önem kazanır.

Takım ölçeği ve kod sahipliği

Küçük bir ekipte, aynı kişilerin aynı dosya setine dokunduğu bir projede “minimal kurallar” hız kazandırır. Büyük ekiplerde ise açık sözleşmeler, standart akışlar ve debug edilebilirlik kritik hale gelir. Redux’un “kuralcı” yapısı bu noktada avantaj, küçük projelerde ise gereksiz yük olabilir.


Context API: Ne zaman yeterli, ne zaman zorlar?

Context, React’in kendi “bağımlılık enjeksiyonu”na benzeyen paylaşım mekanizmasıdır. Tema, dil, authenticated user gibi sınırlı sayıda global değeri aktarmak için mükemmeldir. Ekstra paket yoktur, öğrenme eğrisi düşüktür ve React ile uyumlu çalışır.

Context’in güçlü olduğu alanlar

  • Uygulama kabuğu: tema, dil, feature flag gibi seyrek değişen değerler
  • Ölçeklenebilir prop drilling çözümü: derin ağaçlarda veri geçirme
  • Kompozisyon: Provider’ları modüler tutarak anlaşılır yapı kurma

Performans ve yeniden render gerçeği

Context’in en sık karşılaşılan tuzağı, Provider value’su değiştiğinde o Context’i tüketen tüm bileşenlerin yeniden render olabilmesidir. Bu “her durumda yavaş” demek değildir; fakat yüksek frekanslı güncellemelerde, özellikle de büyük ağaçlarda maliyet hissedilir. Çözüm, state’i parçalara ayırmak, memoization kullanmak ve consumer’ları daha küçük Context’lere bölmektir.

Context Provider zinciri büyüdükçe yeniden render etkisini azaltmaya yönelik pratik bir yapı örneği

Context ile örnek: kimlik ve tercihleri bölmek

Aşağıdaki örnekte, “auth” ve “preferences” ayrı Context’lere ayrılır. Böylece tema değişince auth tüketicileri, kullanıcı değişince tema tüketicileri gereksiz yere etkilenmez. Ayrıca value nesnelerini useMemo ile sabitlemek, referans değişimlerini kontrol eder.

import React, { createContext, useMemo, useContext, useState } from 'react';

const AuthContext = createContext(null);
const PrefContext = createContext(null);

export function AppProviders({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');

  const authValue = useMemo(() => ({ user, setUser }), [user]);
  const prefValue = useMemo(() => ({ theme, setTheme }), [theme]);

  return (
    <AuthContext.Provider value={authValue}>
      <PrefContext.Provider value={prefValue}>
        {children}
      </PrefContext.Provider>
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be used within AppProviders');
  return ctx;
}

export function usePref() {
  const ctx = useContext(PrefContext);
  if (!ctx) throw new Error('usePref must be used within AppProviders');
  return ctx;
}

Bu yaklaşım, küçük/orta ölçekli uygulamalarda gayet iş görür. Ancak state, çok sayıda feature tarafından paylaşılmaya ve karmaşık iş kuralları taşımaya başladığında, Context ile “mini state manager” yazdığınızı fark edebilirsiniz. Bu noktada Redux veya Zustand gibi araçlar daha net bir model sunar.


Redux (Redux Toolkit): Büyük ölçeklerde disiplin ve izlenebilirlik

Redux, tek yönlü veri akışı ve merkezi store fikrini net kurallarla uygular. Özellikle çok ekipli projelerde, “state nerede değişti?” sorusunu cevaplamak, code review yapmak ve hata ayıklamak için güçlü bir çerçeve sağlar. Modern Redux kullanımı neredeyse her zaman Redux Toolkit ile yapılır; boilerplate’i azaltır, iyi pratikleri varsayılan hale getirir.

Neden Redux: standart akış ve güçlü debug

Redux’un artısı, herkesin aynı biçimde düşünmesini sağlamasıdır: action -> reducer -> yeni state. Bu akış, zaman yolculuğu debug’ı, deterministik state geçişleri ve test edilebilir reducer’lar gibi avantajlar doğurur. Özellikle karmaşık senaryolarda “olay günlüğü” gibi çalışır.

Ne zaman ağır gelir?

Redux, bir miktar yapı ve terminoloji gerektirir. Küçük projelerde sırf “ileride lazım olur” diye kurmak, geliştirme hızını düşürebilir. Ayrıca her şeyi store’a atmak, local state’in basitliğini kaybettirir. Bu yüzden Redux’u “global ve kritik” state için düşünmek daha sağlıklıdır.

Redux Toolkit ile gerçekçi örnek: cart slice

Bir alışveriş sepeti, state bütünlüğü ve iş kuralları açısından iyi bir örnektir. Aşağıdaki slice, ekle/çıkar/toplam hesaplama gibi işleri tek yerde toplar. UI bileşenleri yalnızca intent’i (action) iletir.

import { configureStore, createSlice, createSelector } from '@reduxjs/toolkit';

const cartSlice = createSlice({
  name: 'cart',
  initialState: { items: {}, coupon: null },
  reducers: {
    addItem(state, action) {
      const { id, price } = action.payload;
      const current = state.items[id];
      state.items[id] = { id, price, qty: (current?.qty || 0) + 1 };
    },
    removeItem(state, action) {
      const id = action.payload;
      const current = state.items[id];
      if (!current) return;
      if (current.qty <= 1) delete state.items[id];
      else current.qty -= 1;
    },
    applyCoupon(state, action) {
      state.coupon = action.payload;
    }
  }
});

export const { addItem, removeItem, applyCoupon } = cartSlice.actions;

export const store = configureStore({
  reducer: { cart: cartSlice.reducer }
});

const selectCart = (s) => s.cart;
export const selectSubtotal = createSelector([selectCart], (cart) =>
  Object.values(cart.items).reduce((sum, i) => sum + i.price * i.qty, 0)
);

Bu örnek, Redux’un “iş kuralını UI’dan ayırma” gücünü gösterir. Büyük ekiplerde, herkesin aynı “slice” modelini takip etmesi tahmin edilebilirlik sağlar. Ancak küçük projelerde aynı disiplin, gereksiz dosya/katman sayısı gibi hissedilebilir.

Tooling, test ve ekip içi görünürlük

Redux’un olgun ekosistemi, yalnızca store kurulumundan ibaret değildir. DevTools ile action geçmişini izlemek, state’in hangi adımda bozulduğunu bulmayı kolaylaştırır. Unit test tarafında reducer’ların saf fonksiyon olması, “verilen state + action => beklenen state” biçiminde hızlı test yazdırır. Ayrıca TypeScript ile slice tiplerinin türetilmesi, büyük kod tabanlarında hataları derleme zamanında yakalamaya yardımcı olur.

Buna karşılık Zustand veya Context kullanırken de test yapılır; ancak olay akışı daha serbest olduğu için standart bir test kalıbı oluşturmak ekibe düşer. Eğer ürününüz denetim izi, geri alınabilir işlem veya kapsamlı logging gibi gereksinimler taşıyorsa, Redux’un sağladığı görünürlük çoğu zaman fark yaratır.


Zustand: Minimal API ile esnek ve performans odaklı

Zustand, “az kural, çok esneklik” yaklaşımıyla öne çıkar. Bir store oluşturur, istediğiniz yerden okur/yazarsınız. Büyük bir mimari çerçeve dayatmadığı için hızlı başlatılır; aynı zamanda selector tabanlı okuma sayesinde performansı kontrol etmek kolaydır.

Zustand’ın parladığı kullanım biçimleri

  • Orta ölçekli ürünler: birkaç global domain state’i, hızlı geliştirme
  • Performans hassas ekranlar: selector ile dar kapsamlı render
  • “Redux kadar kural istemiyorum” diyen ekipler

Selector ve subscribe mantığı

Zustand’da bileşenler store’un tamamına değil, seçtikleri parçaya abone olur. Bu, state’in sık değiştiği durumlarda bile gereksiz render’ı azaltır. Ayrıca store fonksiyonları, bir sınıf yapısı olmadan, doğal JavaScript fonksiyonları gibi yazılır.

Zustand store'da selector kullanarak yalnızca gereken state'i okuyan bileşen düzeni

Zustand ile örnek: kullanıcı ve bildirim sayacı

Aşağıdaki store, kullanıcı bilgisini ve okunmamış bildirim sayısını yönetir. UI tarafında selector kullanarak yalnızca gerekli alanlar okunur. Persist veya devtools gibi eklentiler ihtiyaca göre eklenebilir.

import { create } from 'zustand';

export const useAppStore = create((set, get) => ({
  user: null,
  unreadCount: 0,
  setUser: (user) => set({ user }),
  incrementUnread: () => set({ unreadCount: get().unreadCount + 1 }),
  clearUnread: () => set({ unreadCount: 0 })
}));

// component usage
// const userName = useAppStore((s) => s.user?.name);
// const unread = useAppStore((s) => s.unreadCount);

Zustand, basitliğiyle hız kazandırır; fakat “hangi kuralı nerede uygularız?” sorusunu ekibe bırakır. Bu iyi bir şey de olabilir, risk de. Eğer ekibinizde farklı stiller hızla çoğalıyorsa, zamanla store yapısı dağınıklaşabilir. Bu durumda minimal bir konvansiyon seti belirlemek önemlidir.

Zustand’da middleware ve kalıcılık pratikleri

Zustand’ın hafifliği, ihtiyaç olduğunda eklentiyle büyümeyi kolaylaştırır. Persist ile belirli alanları localStorage’a yazmak, devtools ile aksiyon benzeri kayıt almak veya immer ile güncellemeleri daha okunur yapmak mümkündür. Burada önemli olan, tüm store’u kalıcı yapmamak ve yalnızca gerçekten gerekli alanları saklamaktır; aksi halde sürüm geçişlerinde veri şeması sorunları doğabilir.


Karar matrisi: Context vs Redux vs Zustand

Seçimi pratikleştirmek için aşağıdaki matrisi kullanın. Buradaki amaç “tek doğru”yu bulmak değil; ihtiyaç profilinizi netleştirmektir. Ayrıca React state management konusu, çoğu zaman tek araçla bitmez: bir arada kullanım yaygındır.

Küçük ürünler ve MVP’ler

Eğer ürününüz MVP ise ve global state azsa, Context çoğu zaman yeterli olur. Özellikle tema/dil/auth gibi alanlarda Context, düşük maliyetli bir başlangıç sağlar. Local state’i yerinde tutup, yalnızca gerçekten paylaşılan alanları yukarı almak hız kazandırır.

Orta ölçek: domain state artıyor, hız hâlâ önemli

Orta ölçekli uygulamalarda, birkaç domain state’i (cart, filters, wizard, permissions) yayılmaya başlar. Bu aşamada Zustand, “Redux’un disiplinine girmeden” merkezi state avantajı sağlar. Selector yapısı, performansı kontrol etmeyi kolaylaştırır. Yine de kritik akışlarda (ödeme, sipariş) state geçişlerini açık tasarlamak gerekir.

Büyük ölçek ve çok ekipli organizasyon

Birden fazla takım aynı store’u paylaşıyorsa, standart akışlar ve denetim mekanizmaları önem kazanır. Redux Toolkit, code review ve onboarding’i kolaylaştıran bir ortak dil sunar. Ayrıca middleware/side-effect yönetimi, logging ve test stratejileri kurumsal ihtiyaçlarda öne çıkar.

Hızlı kontrol listesi

  1. State’in tüketicisi çok mu? Fan-out yüksekse merkezi store düşün.
  2. Değişim çok mu sık? Selector ve ince abonelik modeli avantaj sağlar.
  3. İş kuralı ağır mı? Deterministik akış ve test edilebilirlik değerli.
  4. Ekip büyüyor mu? Ortak konvansiyon ve tooling maliyetin önüne geçer.

Bu noktada daha sistemli bir eğitim planı arıyorsanız, uygulamalı örneklerle derinleşmek için React Eğitimi sayfasına göz atabilirsiniz.


Birlikte kullanım: tek araca mahkûm değilsin

Gerçek projelerde “hepsi ya da hiçbiri” nadirdir. Örneğin Context, tema/dil gibi uygulama kabuğu için kullanılırken; domain state Zustand veya Redux ile yönetilebilir. Sunucu verileri içinse TanStack Query gibi çözümler devreye girer. Burada kritik olan, sınırları net çizmek ve aynı probleme iki farklı araçla saldırmamaktır.

Örnek kombinasyonlar

  • Context + Zustand: kabuk ayarları Context, domain state Zustand
  • Context + Redux: auth/tema Context, iş kuralları ve event akışı Redux
  • Redux + Query: client state Redux, server state Query ile cache

Geçiş stratejisi: “büyük taşıma” yerine kademeli refactor

Mevcut projede Context ile başlandıysa ve büyüdüyse, her şeyi bir anda Redux’a taşımak risklidir. Önce en çok problem çıkaran alanı seçin: örneğin filtreleme ve liste state’i. Oradan başlayarak kademeli geçiş yapın. Aynı şekilde Redux’tan Zustand’a geçişte de “slice slice” ilerlemek mümkün olabilir; fakat tooling ve debug alışkanlıklarını hesaba katmak gerekir.

SSR, streaming ve modern React uyumu

Next.js gibi SSR/streaming senaryolarında state’in sunucu ve istemci sınırında nasıl taşındığı önemlidir. Context genellikle “request başına Provider” mantığıyla çalıştırılabildiği için doğal bir uyum sağlar. Redux’ta ise store’un her istekte yeniden yaratılması, hydrate akışının doğru kurgulanması gerekir. Zustand tarafında da store oluşturma stratejisi (singleton mı, request scoped mu) netleşmelidir. Kısacası, seçim yaparken yalnızca istemci tarafını değil, deployment mimarinizi de düşünün.


Sık yapılan hatalar ve pratik öneriler

Her şeyi global yapmak

En pahalı hata, local state’i sırf “erişimi kolay” diye global store’a taşımaktır. Bu, bağımlılıkları büyütür, değişikliklerin etkisini genişletir ve test yüzeyini artırır. Local kalabilen state’i local tutmak, sürdürülebilirliği artırır.

Context’i “state manager”a çevirmek

Context üzerinde reducer, action tipi, middleware benzeri yapılar kurmak mümkündür; ama bir noktadan sonra kendi Redux’unuzu yazmaya başlarsınız. Eğer ihtiyaçlarınız logging, time-travel, standart akışlar ve geniş ekosistem ise, hazır çözüm daha az risklidir.

Zustand’da konvansiyon eksikliği

Zustand hızlıdır; fakat ekipte “store dosyaları nereye koyulur, isimlendirme nasıl yapılır, side-effect nerede yönetilir?” gibi kararlar alınmazsa, zamanla dağınıklaşır. Basit bir klasör yapısı ve “domain store” ayrımı çoğu sorunu çözer. Özellikle selector kullanımını ekip standardı haline getirmek, performans sürprizlerini azaltır.

Redux’ta gereksiz kompleks mimari

Redux kullanırken, her şeyi action/reducer ile modellemek cazip gelebilir. Ancak bazı UI state’leri (aç/kapa, hover, lokal input) store’a taşındığında gereksiz gürültü oluşur. Redux’u kritik ve paylaşılan state için saklamak, kod tabanını temiz tutar.

Sonuç olarak seçim, araçların “iyi/kötü” olmasıyla değil; sizin problem tanımınızla ilgilidir. React state management dünyasında Context, Redux ve Zustand; doğru yerde kullanıldığında birbirini tamamlayan seçeneklerdir. İhtiyaç profilinizi netleştirin, küçük başlayın, ölçün ve büyüdükçe daha disiplinli bir yapıya geçin.

 VERİ AKADEMİ