REACT / NEXT.JS // HOOKSD::03 ORTA
22m READCOMPLETION: 83%ID::RCT-201

REACT HOOKS DERİNLEMESİNE VE CUSTOM HOOKS

useReducer, useContext, custom hook'lar ve performans optimizasyonu

React Hooks, fonksiyon bileşenlerine durum ve yaşam döngüsü yönetimi kazandırır. Bu derste useState, useReducer, useContext ve özel hook'ları derinlemesine inceliyoruz; ardından performans hook'larıyla optimizasyon yapıyoruz.

useState vs useReducer

// TSX //
// useState — basit durum
const [sayac, setSayac] = useState(0);
 
// useReducer — karmaşık durum mantığı için
type Aksiyon =
  | { tur: 'ARTTIR' }
  | { tur: 'AZALT' }
  | { tur: 'SIFIRLA' }
  | { tur: 'DEGER_ATA'; deger: number };
 
function sayacAzaltici(durum: number, aksiyon: Aksiyon): number {
  switch (aksiyon.tur) {
    case 'ARTTIR':    return durum + 1;
    case 'AZALT':     return Math.max(0, durum - 1);
    case 'SIFIRLA':   return 0;
    case 'DEGER_ATA': return aksiyon.deger;
    default:          return durum;
  }
}
 
function Sayac() {
  const [sayac, dispatch] = useReducer(sayacAzaltici, 0);
 
  return (
    <div>
      <p>Sayaç: {sayac}</p>
      <button onClick={() => dispatch({ tur: 'ARTTIR' })}>+1</button>
      <button onClick={() => dispatch({ tur: 'AZALT' })}>-1</button>
      <button onClick={() => dispatch({ tur: 'SIFIRLA' })}>Sıfırla</button>
    </div>
  );
}

useContext — Global Durum

// TSX //
// context/AuthContext.tsx
import { createContext, useContext, useReducer, useCallback } from 'react';
 
interface KullaniciDurumu {
  kullanici: Kullanici | null;
  yukleniyor: boolean;
}
 
type AuthAksiyon =
  | { tur: 'GIRIS_BASARILI'; kullanici: Kullanici }
  | { tur: 'CIKIS' }
  | { tur: 'YUKLENIYOR' };
 
function authAzaltici(durum: KullaniciDurumu, aksiyon: AuthAksiyon): KullaniciDurumu {
  switch (aksiyon.tur) {
    case 'GIRIS_BASARILI': return { kullanici: aksiyon.kullanici, yukleniyor: false };
    case 'CIKIS':          return { kullanici: null, yukleniyor: false };
    case 'YUKLENIYOR':     return { ...durum, yukleniyor: true };
  }
}
 
interface AuthContext {
  durum: KullaniciDurumu;
  girisYap: (email: string, sifre: string) => Promise<void>;
  cikisYap: () => void;
}
 
const AuthCtx = createContext<AuthContext | null>(null);
 
export function AuthSaglayici({ children }: { children: React.ReactNode }) {
  const [durum, dispatch] = useReducer(authAzaltici, {
    kullanici: null,
    yukleniyor: false,
  });
 
  const girisYap = useCallback(async (email: string, sifre: string) => {
    dispatch({ tur: 'YUKLENIYOR' });
    const res = await fetch('/api/auth/giris', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, sifre }),
    });
    const { kullanici } = await res.json();
    dispatch({ tur: 'GIRIS_BASARILI', kullanici });
  }, []);
 
  const cikisYap = useCallback(() => {
    dispatch({ tur: 'CIKIS' });
  }, []);
 
  return (
    <AuthCtx.Provider value={{ durum, girisYap, cikisYap }}>
      {children}
    </AuthCtx.Provider>
  );
}
 
// Custom hook — null güvenliği
export function useAuth() {
  const ctx = useContext(AuthCtx);
  if (!ctx) throw new Error('useAuth, AuthSaglayici içinde kullanılmalıdır');
  return ctx;
}

Özel Hooks (Custom Hooks)

// TSX //
// hooks/useLocalStorage.ts
function useLocalStorage<T>(anahtar: string, ilkDeger: T) {
  const [deger, setDeger] = useState<T>(() => {
    try {
      const kayit = window.localStorage.getItem(anahtar);
      return kayit ? JSON.parse(kayit) : ilkDeger;
    } catch {
      return ilkDeger;
    }
  });
 
  const kaydet = useCallback((yeniDeger: T | ((prev: T) => T)) => {
    setDeger(prev => {
      const hesaplanan = typeof yeniDeger === 'function'
        ? (yeniDeger as (prev: T) => T)(prev)
        : yeniDeger;
      window.localStorage.setItem(anahtar, JSON.stringify(hesaplanan));
      return hesaplanan;
    });
  }, [anahtar]);
 
  return [deger, kaydet] as const;
}
 
// hooks/useFetch.ts
interface FetchDurumu<T> {
  veri: T | null;
  yukleniyor: boolean;
  hata: string | null;
}
 
function useFetch<T>(url: string) {
  const [durum, setDurum] = useState<FetchDurumu<T>>({
    veri: null, yukleniyor: true, hata: null,
  });
 
  useEffect(() => {
    let iptal = false;
    setDurum({ veri: null, yukleniyor: true, hata: null });
 
    fetch(url)
      .then(r => {
        if (!r.ok) throw new Error(`HTTP ${r.status}`);
        return r.json() as Promise<T>;
      })
      .then(veri => {
        if (!iptal) setDurum({ veri, yukleniyor: false, hata: null });
      })
      .catch(e => {
        if (!iptal) setDurum({ veri: null, yukleniyor: false, hata: e.message });
      });
 
    return () => { iptal = true; }; // Cleanup — memory leak önle
  }, [url]);
 
  return durum;
}
 
// hooks/useDebounce.ts
function useDebounce<T>(deger: T, gecikmems: number): T {
  const [geciktirilmis, setGeciktirilmis] = useState(deger);
 
  useEffect(() => {
    const zamanlayici = setTimeout(() => setGeciktirilmis(deger), gecikmems);
    return () => clearTimeout(zamanlayici);
  }, [deger, gecikmems]);
 
  return geciktirilmis;
}
 
// Kullanım — arama kutusu
function AramaKutusu() {
  const [aramaMetni, setAramaMetni] = useState('');
  const geciktirilmisArama = useDebounce(aramaMetni, 300);
 
  const { veri: sonuclar } = useFetch<Sonuc[]>(
    geciktirilmisArama ? `/api/ara?q=${geciktirilmisArama}` : '/api/ara'
  );
  // ...
}

useMemo ve useCallback

// TSX //
// useMemo — pahalı hesaplamayı önbelleğe al
function UrunListesi({ urunler, filtre }: { urunler: Urun[]; filtre: string }) {
  // filtre değişmediği sürece tekrar çalışmaz
  const filtrelenmisler = useMemo(
    () => urunler.filter(u =>
      u.isim.toLowerCase().includes(filtre.toLowerCase())
    ),
    [urunler, filtre]
  );
 
  const toplamFiyat = useMemo(
    () => filtrelenmisler.reduce((acc, u) => acc + u.fiyat, 0),
    [filtrelenmisler]
  );
 
  return (
    <div>
      <p>Toplam: ₺{toplamFiyat}</p>
      {filtrelenmisler.map(u => <UrunKart key={u.id} urun={u} />)}
    </div>
  );
}
 
// useCallback — fonksiyon referansını sabitle
const UrunKart = React.memo(({ urun, onSil }: { urun: Urun; onSil: (id: number) => void }) => {
  console.log('UrunKart render:', urun.id);
  return (
    <div>
      <span>{urun.isim}</span>
      <button onClick={() => onSil(urun.id)}>Sil</button>
    </div>
  );
});
 
function Sepet() {
  const [urunler, setUrunler] = useState<Urun[]>([]);
 
  // useCallback olmadan her render'da yeni fonksiyon → React.memo etkisiz
  const urununuSil = useCallback((id: number) => {
    setUrunler(prev => prev.filter(u => u.id !== id));
  }, []); // Bağımlılık yok — stale closure riski için setUrunler(prev=>) kullan
 
  return urunler.map(u => <UrunKart key={u.id} urun={u} onSil={urununuSil} />);
}

useRef — DOM ve Değişken Değerler

// TSX //
function VideoOynatici() {
  const videoRef = useRef<HTMLVideoElement>(null);
  const oynatiliyorRef = useRef(false); // render tetiklemez
 
  function oynat() {
    videoRef.current?.play();
    oynatiliyorRef.current = true;
  }
 
  function durdur() {
    videoRef.current?.pause();
    oynatiliyorRef.current = false;
  }
 
  // Önceki değeri sakla
  function usePrevious<T>(deger: T): T | undefined {
    const ref = useRef<T>();
    useEffect(() => { ref.current = deger; });
    return ref.current;
  }
 
  return (
    <div>
      <video ref={videoRef} src="/video.mp4" />
      <button onClick={oynat}>Oynat</button>
      <button onClick={durdur}>Durdur</button>
    </div>
  );
}

Compound Components Deseni

// TSX //
// Bileşen API'sini temiz tutan desen
interface TabDurumu { aktif: string; setAktif: (id: string) => void; }
const TabCtx = createContext<TabDurumu | null>(null);
 
function Tabs({ varsayilan, children }: { varsayilan: string; children: React.ReactNode }) {
  const [aktif, setAktif] = useState(varsayilan);
  return <TabCtx.Provider value={{ aktif, setAktif }}>{children}</TabCtx.Provider>;
}
 
function TabListesi({ children }: { children: React.ReactNode }) {
  return <div role="tablist" className="flex border-b">{children}</div>;
}
 
function Tab({ id, children }: { id: string; children: React.ReactNode }) {
  const { aktif, setAktif } = useContext(TabCtx)!;
  return (
    <button
      role="tab"
      aria-selected={aktif === id}
      onClick={() => setAktif(id)}
      className={aktif === id ? 'border-b-2 border-blue-500' : ''}
    >
      {children}
    </button>
  );
}
 
function TabPanel({ id, children }: { id: string; children: React.ReactNode }) {
  const { aktif } = useContext(TabCtx)!;
  return aktif === id ? <div role="tabpanel">{children}</div> : null;
}
 
// Alt bileşenleri ana bileşene ekle
Tabs.Listesi = TabListesi;
Tabs.Tab    = Tab;
Tabs.Panel  = TabPanel;
 
// Kullanım — temiz API
<Tabs varsayilan="genel">
  <Tabs.Listesi>
    <Tabs.Tab id="genel">Genel</Tabs.Tab>
    <Tabs.Tab id="guvenlik">Güvenlik</Tabs.Tab>
  </Tabs.Listesi>
  <Tabs.Panel id="genel"><GenelAyarlar /></Tabs.Panel>
  <Tabs.Panel id="guvenlik"><GuvenlikAyarlar /></Tabs.Panel>
</Tabs>

Sonuç

React Hooks ekosistemi; useReducer ile öngörülebilir durum, useContext ile bileşenler arası veri paylaşımı, özel hook'lar ile mantık yeniden kullanımı ve useMemo/useCallback ile performans optimizasyonu sağlar. Compound Components deseni ise esnek ve kullanımı kolay bileşen API'leri oluşturmanıza olanak tanır. Bir sonraki derste Next.js ile full-stack uygulama geliştirmeyi inceleyeceğiz.