Redux · RTK · React Query
Ortak konu · İlan 1 İlan 3

Redux · Redux Toolkit · React Query

Üçü de "state yönetimi" kelimesinin altında toplanır ama iki farklı problemi çözerler: Redux/RTK client state için (UI'da tutulan, sunucudan gelmeyen veri — sepet, form, açık/kapalı paneller), React Query ise server state için (API'den gelen, cache'lenmesi ve senkronize kalması gereken veri).

Canlı demo — gerçek Redux Toolkit store'u

Bu, sahte bir animasyon değil — sayfanın kendisi @reduxjs/toolkit ve react-redux paketleriyle kurulu gerçek bir store çalıştırıyor. Butonlara bas, sağdaki panelde her tıklamanın hangi action'ı dispatch ettiğini canlı gör.

🛒 Mini sepet (gerçek @reduxjs/toolkit store)

Kablosuz Kulaklık₺1.200
Mekanik Klavye₺2.400
USB-C Hub₺650
Sepet boş — yukarıdan ürün ekle.
Toplam₺0
dispatch edilen action'lar
Store başlatıldı — sepet boş.

Bu demonun kaynak kodu — 3 dosya

Redux Toolkit'in klasik üçlüsü: slice (state + reducer + action'lar bir arada), store (slice'ları birleştirir), component (hook'larla bağlanır).

cartSlice.ts
// cartSlice.ts — bu sayfadaki canlı demoyu çalıştıran gerçek kod
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";

interface CartItem { id: string; name: string; price: number; qty: number }
interface CartState { items: CartItem[]; log: string[] }

const initialState: CartState = { items: [], log: [] };

const cartSlice = createSlice({
  name: "cart",
  initialState,
  reducers: {
    // Redux Toolkit altında Immer var: state'i "mutate" ediyormuş gibi
    // yazarsın ama aslında değişmez (immutable) bir kopya üretilir.
    addItem(state, action: PayloadAction<{id:string; name:string; price:number}>) {
      const existing = state.items.find(i => i.id === action.payload.id);
      if (existing) existing.qty += 1;
      else state.items.push({ ...action.payload, qty: 1 });
    },
  },
});

export const { addItem } = cartSlice.actions;
export default cartSlice.reducer;
store.ts
// store.ts
import { configureStore } from "@reduxjs/toolkit";
import cartReducer from "./cartSlice";

export const store = configureStore({
  reducer: { cart: cartReducer },
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
ProductCard.tsx
// bir component içinde kullanım
import { useDispatch, useSelector } from "react-redux";
import { addItem } from "./cartSlice";
import type { RootState, AppDispatch } from "./store";

function ProductCard({ product }) {
  const dispatch = useDispatch<AppDispatch>();
  const qty = useSelector((s: RootState) =>
    s.cart.items.find(i => i.id === product.id)?.qty ?? 0
  );
  return <button onClick={() => dispatch(addItem(product))}>Sepete ekle ({qty})</button>;
}
💡
Neden düz Redux değil de Redux Toolkit?Klasik Redux'ta action type sabitleri, action creator'lar ve switch/case reducer'ları elle yazardın — çok boilerplate. RTK bunu createSlice ile tek fonksiyona indirir, immutable update'i Immer ile otomatikleştirir. 2025'te "Redux" denince artık pratikte herkes RTK'yı kastediyor.

Akış: dispatch → reducer → store → UI

AdımNe olur
1. Dispatchdispatch(addItem({id:"p1",...})) — component bir "bu oldu" olayı gönderir
2. ReducerStore, ilgili slice'ın reducer'ını mevcut state + action ile çağırır, yeni state üretir
3. Store güncellerYeni state store içinde saklanır, tüm subscriber'lara haber verilir
4. UI yeniden renderuseSelector ile o state'i okuyan component'ler otomatik güncellenir

React Query — sunucu verisi için farklı bir araç

Redux'ta sunucudan gelen veriyi de state'e koyup elle loading/error/cache/refetch yönetmeye çalışmak — bugün "anti-pattern" sayılıyor. React Query bunu senin yerine yapar: cache, arka planda yeniden çekme, retry, invalidation.

KampanyaListesi.tsx
// React Query (TanStack Query) — SUNUCU verisini yönetir, client state değil
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

function KampanyaListesi() {
  const { data, isLoading, error } = useQuery({
    queryKey: ["kampanyalar"],
    queryFn: () => fetch("/api/kampanyalar").then(r => r.json()),
    staleTime: 60_000, // 1 dk boyunca "taze" say, tekrar fetch etme
  });

  const qc = useQueryClient();
  const { mutate: kampanyaEkle } = useMutation({
    mutationFn: (yeni) => fetch("/api/kampanyalar", { method: "POST", body: JSON.stringify(yeni) }),
    onSuccess: () => qc.invalidateQueries({ queryKey: ["kampanyalar"] }), // cache'i geçersiz kıl → yeniden çek
  });

  if (isLoading) return <p>Yükleniyor…</p>;
  if (error) return <p>Hata oluştu.</p>;
  return <ul>{data.map(k => <li key={k.id}>{k.ad}</li>)}</ul>;
}
Redux ToolkitReact Query
Client state: sepet, form, tema, modal açık/kapalıServer state: API'den gelen liste, detay, kullanıcı profili
Sen ne zaman güncelleneceğine karar verirsinKütüphane otomatik cache'ler, stale olunca kendi yeniler
Senkron, tahmin edilebilirAsenkron, network'e bağlı
⚠️
Mülakat tuzağı"Neden hem Redux hem React Query kullanıyorsunuz, ikisi de state yönetmiyor mu?" sorusuna "ikisi farklı state türü için var, birbirinin yerine geçmez" cevabı seni senior olarak ayırt eder. Junior cevabı: "ikisi de aynı şey".

Egzersizler

1) Az önceki demoya 'toplam ürün sayısını sıfırlamadan sepeti %10 indirimli göster' özelliği eklesen, bunu slice'a mı, component'e mi (selector ile hesaplanan türetilmiş değer) koyardın? Neden?
Component/selector'a. Slice'ta "ham veriyi" tut (fiyat, adet); indirimli toplam gibi türetilmiş (derived) değerleri state'e kaydetme — useSelector içinde hesapla. Aksi halde state ile türetilmiş değer birbirinden bağımsız kayıp senkronizasyon bug'larına açık hale gelir.
2) createSlice içindeki reducer'da state.items.push(...) yazmak Redux'un 'immutability' kuralını çiğnemiyor mu?
Görünüşte evet ama hayır — RTK, reducer'ları Immer'in produce fonksiyonuyla sarmalar. Sen bir "draft" obje üzerinde mutate ediyormuş gibi yazarsın, Immer arkada gerçek immutable güncellemeyi üretir. Düz Redux'ta bunu elle {...state, items:[...state.items, yeni]} şeklinde yapman gerekirdi.