Skip to content

Bölüm 9 / 16

Durum yönetimi

Zustand, Redux Toolkit; ne zaman gerekir.

11 dakikalık okuma

10. hattın 12. dersinde verilen söz burada ödeniyor: NgRx'i "Redux'un Angular uyarlaması" diye tanıtmıştık — bu ders Redux'un kendi evine, React'e döner. Ama kütüphaneden önce karar gelir: karar merdiveni (local → lifted → context → store), hangi belirtinin hangi basamağa çıkardığını söyler; store en üst basamaktır, varsayılan değil. Sonra iki aracı tanıyorsun: Zustand (selector'lı, törensiz küçük store) ve Redux Toolkit (createSlice, immer, DevTools — NgRx ile aynı desenin aslı). Kapanış ilkesi 10. hattakiyle aynıdır: her şey store'a girmez — sunucu verisi 10. derse, ekran adresi 11. derse gider.

01Ne öğreneceksin

  • Karar merdiveni: local → lifted → context → store — hangi belirti hangi basamağa çıkarır
  • Zustand: create, eylem fonksiyonları, selector'la seçici abonelik (context'in eksiği burada kapanır)
  • Redux Toolkit: configureStore + createSlice; immer'in değişmezlik sözleşmesiyle bağı
  • NgRx köprüsü: aynı desen, iki uyarlama — 10. hattın Redux kıyası sözü ödenir
  • "Her şey store'a girmez": form taslağı, sunucu verisi, URL durumu nereye gider; RTK Query'nin adı
  • Araç seçimi: hangi projede Zustand, hangisinde Redux Toolkit, hangisinde hiçbiri

02Karar merdiveni: store bir varış değil, son basamak

10. hattın 12. dersindeki merdivenin React hâli. Her basamağın bir belirtisi vardır; belirti yokken basamak çıkmak, mimariyi süs için taşımaktır:

BasamakBelirtiAraç
YerelDurum tek bileşenin derdiuseState / useReducer (2. ve 6. ders)
Yukarı taşınmışİki kardeş aynı veriyi görmeliLifting + tek doğruluk kaynağı (2. ders)
ContextAğaç geneli, az değişen değer; drilling bileşimle çözülemediContext + reducer (8. ders)
StoreÇok bileşen + sık güncelleme + seçici abonelik ihtiyacı; geçmiş/araç izleme (DevTools) isteği; context zincirleri dolaşmaya başladıZustand / Redux Toolkit (bu ders)

Somut çeviri: başvuru formunun taslağı yerel kalır; filtre, listeyle paylaşılmak için yukarı taşınır; oturum ve tema context'tedir; "birçok ekranın okuyup yazdığı, sık değişen, geçmişi izlenmek istenen" istemci durumu — çok adımlı bir sihirbazın ilerleyişi, uygulama geneli bildirim kuyruğu, karmaşık bir düzenleyicinin durumu — store'a çıkar. Merdivenin altın kuralı 10. hattakiyle aynıdır: bir üst basamağa ancak alt basamağın acısı görülünce çıkılır; "ileride lazım olur" diye kurulmuş store, projeye maliyetini ilk günden ödetir.

03Zustand: törensiz store

Zustand, "store" fikrinin en küçük dürüst hâlidir: modül seviyesinde bir create çağrısı, state + eylemleri tek nesnede toplar; Provider yoktur, bileşen store'a bir hook'la bağlanır:

import { create } from "zustand";

type BildirimDurumu = {
  bildirimler: Bildirim[];
  seciliId: number | null;
  bildirimGeldi: (b: Bildirim) => void;
  secimDegisti: (id: number | null) => void;
};

export const useBildirimStore = create<BildirimDurumu>()((set) => ({
  bildirimler: [],
  seciliId: null,
  bildirimGeldi: (b) =>
    set((d) => ({ bildirimler: [...d.bildirimler, b] })),   // değişmezlik bizde (2. ders)
  secimDegisti: (id) => set({ seciliId: id }),              // kısmi nesne: geri kalanı korunur
}));

Asıl olay bileşen tarafındadır — selector:

// yalnız SAYIYI seçiyorum: bildirim listesi değişmedikçe render YOK
const sayi = useBildirimStore((d) => d.bildirimler.length);

// yalnız eylemi seçiyorum: eylem referansı sabittir → bu bileşen HİÇ render olmaz
const bildirimGeldi = useBildirimStore((d) => d.bildirimGeldi);

Selector, 8. derste context'in veremediği şeyi verir: store değiştiğinde herkes değil, seçtiği dilim Object.is ile değişenler render olur — abonelik köprüsü, 7. derste tanıdığın useSyncExternalStore ile kuruludur. İki kıdem notu: selector'dan render içinde yeni nesne türetme (d => d.bildirimler.filter(...) her seferinde yeni referans döndürür ve her yayında render yersin — türetmeyi useMemo'ya ya da store'a al); ve store modül seviyesi bir tekil olduğu için sunucu tarafında render'da (12. hatta göreceğin SSR) istekler arası sızıntı riski taşır — Zustand'ın SSR kılavuzu bu yüzden store'u istek başına kurdurur. Küçük-orta ölçekli istemci durumu için bugün topluluğun varsayılanı budur.

04Redux Toolkit: desenin aslı, modern ambalajda

Redux, 10. hattın 12. dersinde NgRx üzerinden öğrendiğin desenin kendisidir: tek store, eylemler (olay, emir değil), saf reducer'lar, tek yönlü akış. Modern Redux Redux Toolkit (RTK) demektir — eski el yazması eylem sabitleri ve switch törenleri yerine createSlice:

import { configureStore, createSlice } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit";

const basvuruSlice = createSlice({
  name: "basvurular",
  initialState: { liste: [] as Basvuru[], seciliId: null as number | null },
  reducers: {
    basvuruEklendi(d, e: PayloadAction<Basvuru>) {
      d.liste.push(e.payload);            // "mutasyon" — immer taslağı üzerinde, MEŞRU
    },
    basvuruSilindi(d, e: PayloadAction<number>) {
      d.liste = d.liste.filter(b => b.id !== e.payload);
      if (d.seciliId === e.payload) d.seciliId = null;   // tutarlılık tek yerde (6. ders)
    },
  },
});
export const { basvuruEklendi, basvuruSilindi } = basvuruSlice.actions;
// eylem üreticileri OTOMATİK: basvuruSilindi(5) → { type: "basvurular/basvuruSilindi", payload: 5 }

export const store = configureStore({
  reducer: { basvurular: basvuruSlice.reducer },
});
// bileşen tarafı: <Provider store={store}> + useSelector / useDispatch

Buradaki push satırı 2. dersin Object.is sözleşmesine ihanet gibi görünür — değildir: createSlice, reducer'ını immer ile sarar. Yazdığın "mutasyon" gerçek nesneye değil bir taslağa işler; immer taslaktan, değişen dallar için yeni referanslar taşıyan yeni bir değişmez nesne üretir. Sözleşme değişmedi, spread zinciri yazma yükü kütüphaneye devredildi — iç içe yapılarda hata kaynağı olan { ...d, ic: { ...d.ic } } merdivenleri kalktı. Bedeli bilinçtir: taslak dışına taslak referansı sızdırmak ve immer'siz bağlamda aynı stili sürdürmek klasik hatalardır.

NgRx köprüsünü tek tabloda kapatalım — 10-12'de "Redux kıyası React'te gelecek" demiştik, işte karşılıklar: NgRx Action ↔ slice eylemi; createReducer ↔ createSlice.reducers; memoized createSelector ↔ RTK'nın da içerdiği aynı reselect kütüphanesi; NgRx Effects ↔ RTK'da thunk/listener ara katmanları (asenkron işin reducer dışına alınması — aynı ilke). Redux DevTools iki dünyada da aynı eklentidir: eylem akışı, durum farkları, zamanda geri sarma. Desen bir, uyarlama iki — mülakatta bu cümle iki hattın da hakkını verir.

05Her şey store'a girmez

10. hattın 12. dersindeki ilkenin React hâli — store kurulduğunda ilk savrulma, her veriyi içine süpürmektir. Üç sınır çizgisi:

  • Form taslağı ve geçici UI durumu (açık panel, imleç, taslak metin) yereldir — store'a taşımak her tuşu uygulama geneli bir olaya çevirir, render yayılımını ve gürültüyü büyütür. 4. dersin controlled formu useState'iyle mutludur.
  • Sunucudan gelen veri store'un en büyük yanlış kiracısıdır. Başvuru listesi senin durumun değil, sunucunun durumunun kopyasıdır: bayatlar, yeniden çekilir, önbelleklenir, sayfalanır — bu yaşam döngüsünü elle store'da yönetmek, 10. derste göreceğin TanStack Query'nin işini elle yapmaktır. RTK'nın kendi cevabının adını bil: RTK Query — Redux dünyasında aynı işi gören veri katmanı; Redux'lu projede onu, değilse TanStack Query'yi seçersin.
  • Ekran adresi olan durum (seçili sekme, filtre, sayfa numarası) URL'de yaşamalıdır — paylaşılabilir, yer imlenebilir, geri tuşuyla gezilebilir. Bu 11. dersin konusudur; store'a koyarsan kullanıcı linki paylaştığında durum kaybolur.

Süzgeçten geçince store'da kalan şey çoğu projede şaşırtıcı kadar azdır: oturum benzeri kimlik bilgisi (context de yeter), uygulama geneli bildirimler, çok adımlı akış ilerleyişi, gerçek zamanlı beslemelerin istemci tamponu. "Store'umuz küçükse doğru yoldayız" — iki hattın ortak kıdem cümlesi.

06Seçim: hangi projede hangisi?

Karar üç soruyla verilir. (1) Store'luk belirti var mı? Yoksa merdivenin alt basamakları yeter — React projelerinin önemli bölümü context + Query ikilisiyle store'suz yaşar. (2) Varsa ekip ve ölçek ne istiyor? Küçük-orta istemci durumu, az tören, hızlı başlangıç → Zustand. Büyük ekip, katı konvansiyon ihtiyacı, zengin ara katman/araç ekosistemi, mevcut Redux bilgisi → Redux Toolkit. (3) Sunucu verisi kim? Cevap her iki durumda da aynı: store değil, veri katmanı (TanStack Query / RTK Query). Mülakat çerçevesi de budur: araç adı saymak değil, belirti → basamak → araç zincirini gerekçeyle yürümek; "Redux bilmek", eylem-reducer-selector desenini bilmektir ve o desen sende iki uyarlamasıyla (NgRx + RTK) var.

07Mini alıştırma — Merdiveni tırman

Yaklaşık 20 dakika · StackBlitz projene zustand ve @reduxjs/toolkit + react-redux paketlerini ekle

  1. Bildirim kuyruğunu Zustand'la kur (bu dersteki store); üstbara yalnız bildirimler.length'i seçen bir sayaç, ayrı bir panele listeyi bağla. Profiler'da kanıtla: seçim değişince sayaç render olmuyor, liste değişince ikisi de oluyor.
  2. Selector tuzağını yaşa: sayacın selector'ını bilerek d => d.bildirimler.filter(b => !b.okundu) yap; her yayında render yediğini gör, sebebini (her çağrıda yeni dizi referansı) yaz ve düzelt.
  3. Aynı bildirim durumunu bu kez RTK ile kur: createSlice + configureStore + useSelector/useDispatch. Reducer'da push kullan ve dışarıdan gelen state referansının yine de her eylemde değiştiğini Object.is kıyasıyla konsolda kanıtla — immer'in işini gör.
  4. "Silinen seçiliyse seçimi düşür" kuralını slice'a taşı (6. dersin reducer alıştırmasının store ölçeği); Redux DevTools eklentisiyle eylem akışını izle, bir eylemi geri sar.
  5. Reducer'ı bileşensiz test et: basvuruSlice.reducer'ı iki eylemle elle çağır, dönen state'leri doğrula — NgRx'te yaptığın testin aynısı olduğunu not et.
  6. Sınır çizgisi antrenmanı: projendeki beş veriyi (form taslağı, oturum, başvuru listesi, seçili sekme, bildirimler) merdiven + üç sınır kuralıyla yerleştir; her biri için tek cümle gerekçe yaz.

08Kendini sına

Karar merdivenini belirtileriyle anlat: hangi durum hangi basamakta yaşar ve "erken store" neden bir tasarım hatasıdır?

Merdiven dört basamaktır ve her birine bir belirti çıkarır. Durum tek bileşenin derdiyse yereldir: useState, geçişler karmaşıksa useReducer. İki kardeş aynı veriyi görmeliyse en yakın ortak ataya taşınır — lifting + tek doğruluk kaynağı. Değer ağacın genelinden okunuyor ve az değişiyorsa (oturum, tema, dil), drilling de bileşimle çözülemiyorsa context + reducer kalıbı. Çok bileşenin okuyup yazdığı, sık güncellenen, seçici abonelik ve araç izleme (DevTools, geçmiş) isteyen istemci durumu ise store basamağıdır — Zustand ya da Redux Toolkit. Kural, bir üst basamağa ancak alttakinin acısı görülünce çıkmaktır. Erken store iki bedel ödetir: her küçük durum değişikliği uygulama geneli tören (eylem, reducer, selector) ister ve store, form taslağı gibi yerel verileri de içine çekerek hem gürültüyü hem render yayılımını büyütür. Projelerin önemli bölümü context + sunucu durumu katmanıyla store'suz yaşar; store'un küçük kalması sağlık işaretidir.

Zustand'ın selector mekanizması context'in hangi eksiğini kapatır? İki selector tuzağını ve store'un tekil doğasının riskini anlat.

Context'te Provider value'su değişince o context'i okuyan tüm bileşenler render olur — seçici abonelik yoktur. Zustand'da bileşen store'a selector'la bağlanır: useStore(d => d.dilim); store her değiştiğinde selector çalışır ama bileşen yalnız kendi seçtiği dilim Object.is ile değiştiyse render olur (köprü useSyncExternalStore ile kurulur). Eylemi seçen bileşen hiç render olmaz çünkü eylem fonksiyonlarının referansı sabittir. Birinci tuzak: selector içinde her çağrıda yeni referans türetmek — filter/map sonucu her yayında "değişti" sayılır ve seçicilik kaybolur; türetme useMemo'ya ya da store'daki hazır bir alana alınır. İkinci tuzak: değişmezlik sorumluluğu sende kalır — set'e verdiğin güncellemede yeni referanslar üretmek zorundasın. Risk: store modül seviyesinde bir tekildir; tarayıcıda sorun değilken sunucu tarafı render'da (SSR) istekler arası durum sızıntısı yaratabilir — SSR'da store istek başına kurulur.

Redux Toolkit'te createSlice reducer'ında d.liste.push(...) yazmak neden meşrudur? İmmer'in mekanizmasını ve değişmezlik sözleşmesiyle ilişkisini anlat.

Çünkü o satır gerçek state'e değil immer'in taslağına işler. createSlice her reducer'ı immer ile sarar: reducer'a gelen d bir Proxy taslaktır; üzerinde yapılan "mutasyonlar" kaydedilir ve immer, reducer bitince değişen dallar için yeni referanslar taşıyan yepyeni bir değişmez nesne üretir — değişmeyen dallar eski referanslarını korur (yapısal paylaşım). Dışarıdan bakan dünya için Object.is sözleşmesi aynen geçerlidir: her eylem, değişen yolda yeni referans demektir; selector'lar ve React'in render kıyasları bozulmaz. Kazanç, iç içe yapılarda hataya açık spread merdivenlerinin ({ ...d, ic: { ...d.ic, alan: x } }) kalkmasıdır. Dikkat isteyen iki nokta: taslak referansını reducer dışına sızdırmamak (taslak, reducer bitince geçersizdir) ve bu yazım stilinin yalnız immer'li bağlamda meşru olduğunu bilmek — sıradan useState güncellemesinde push hâlâ 2. dersin ihlalidir.

"Sunucudan gelen liste Redux store'unda dursun" diyen ekip kararına itirazını yap; NgRx-RTK köprüsünü ve RTK Query'nin yerini de bağla.

İtiraz sınıflandırmadan başlar: başvuru listesi istemci durumu değil, sunucu durumunun kopyasıdır — sahibi sunucudur, bayatlar, yeniden çekilmek, önbelleklenmek, sayfalanmak ve istekler arasında paylaşılmak ister. Bu yaşam döngüsünü store'da elle kurmak (yükleniyor/hata bayrakları, önbellek anahtarları, yeniden getirme zamanlaması) özel araçların işini el emeğiyle yapmaktır ve store'u asıl işinden uzaklaştırır. Doğru adres bir veri katmanıdır: bağımsız projede TanStack Query; Redux'lu projede Redux ekosisteminin kendi cevabı RTK Query — önbellek, geçersiz kılma ve otomatik hook üretimiyle aynı işi store'un yanında görür. Köprü cümlesi: Redux ve NgRx aynı desenin iki uyarlamasıdır — eylem = olay, saf reducer, memoized selector (ikisinde de reselect fikri), asenkron işin reducer dışına alınması (Effects ↔ thunk/listener) ve aynı Redux DevTools. Bu yüzden karar "Redux'a mı NgRx'e mi yatırım" değil, desene yatırımdır; sunucu verisi ise her iki dünyada da store'un değil veri katmanının kiracısıdır.

09Sınav köşesi

Yazılıda nasıl sorulur

(1) Durum yönetimi karar merdiveninin basamakları nelerdir? (2) Hangi belirtiler store ihtiyacını gösterir? (3) Zustand'da selector ne işe yarar; bileşen ne zaman render olur? (4) Zustand ile Context API'nin render davranışı farkı nedir? (5) Redux'un üç temel ilkesi nedir (tek store, salt okunur state, saf reducer)? (6) createSlice ne üretir; eylem tipleri nereden gelir? (7) İmmer ne yapar; değişmezlik sözleşmesi neden bozulmaz? (8) NgRx ile Redux Toolkit'in ortak deseni ve karşılıkları nelerdir? (9) Sunucu verisi neden store'a konmaz; RTK Query nedir? (10) Hangi durum URL'de yaşamalıdır?

Sözlüde nasıl anlatılır — 2 dakikalık taslak

Düzen: (1) Merdiven — "Durum yönetimini merdivenle anlatırım: yerel → yukarı taşınmış → context → store; üst basamağa ancak alttakinin acısı görülünce çıkarım, store varsayılan değil son basamaktır." (2) Zustand — "Törensiz store: Provider yok, bileşen selector'la bağlanır ve yalnız seçtiği dilim değişince render olur — context'in seçici abonelik eksiğini kapatır." (3) RTK — "Modern Redux, Toolkit demektir: createSlice eylem üreticilerini otomatik çıkarır, immer sayesinde taslak üzerinde mutasyon yazarım ama dışarıya değişmez yeni referanslar çıkar — Object.is sözleşmesi korunur." (4) Köprü — "NgRx'te öğrendiğim desenin aslı: eylem = olay, saf reducer, memoized selector, aynı DevTools; desen bir, uyarlama iki." (5) Kıdem vuruşu — "Her şey store'a girmez: form taslağı yerel, sunucu verisi Query/RTK Query'de, ekran adresi URL'de — store küçükse doğru yoldayız."