İçeriğe geç
react

Kendi React Hook'larınızı Yazma: Pratik Örneklerle Rehber

Tarık Tunç
Kendi React Hook'larınızı Yazma: Pratik Örneklerle Rehber

Kendi React Hook'larınızı Yazma: Pratik Örneklerle Rehber

Custom hook'lar, React'in en güçlü özelliklerinden biridir. Bileşenler arasında tekrarlayan state mantığını paylaşmanın, kodunuzu temiz ve yeniden kullanılabilir tutmanın standart yoludur. Bu rehberde custom hook yazmanın kurallarını, yaygın pattern'leri ve gerçek dünya örneklerini adım adım ele alacağız.

Custom Hook Nedir?

Custom hook, adı use ile başlayan ve içinde diğer React hook'larını kullanan bir JavaScript fonksiyonudur. Class component dönemindeki HOC (Higher-Order Component) ve render props pattern'lerinin modern ve çok daha temiz alternatifidir.

Custom hook'lar state'i değil, state mantığını paylaşır. İki farklı bileşen aynı custom hook'u kullandığında, her biri kendi bağımsız state'ine sahip olur.

Kural: use ile Başlamalı

React'in hook kuralları custom hook'lar için de geçerlidir:

  • Fonksiyon adı use ile başlamalıdır (React linter'ın hook kurallarını uygulayabilmesi için)
  • Yalnızca bileşenlerin veya diğer hook'ların en üst seviyesinde çağrılabilir
  • Döngüler, koşullar veya iç içe fonksiyonlar içinde çağrılamaz

Pratik Örnekler

1. useLocalStorage - Kalıcı State

import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  // Lazy initialization ile localStorage'dan okuma
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`useLocalStorage okuma hatası [${key}]:`, error);
      return initialValue;
    }
  });

  // Değer değiştiğinde localStorage'a yazma
  useEffect(() => {
    try {
      localStorage.setItem(key, JSON.stringify(storedValue));
    } catch (error) {
      console.error(`useLocalStorage yazma hatası [${key}]:`, error);
    }
  }, [key, storedValue]);

  // Değeri kaldırma fonksiyonu
  const removeValue = () => {
    localStorage.removeItem(key);
    setStoredValue(initialValue);
  };

  return [storedValue, setStoredValue, removeValue];
}

// Kullanım
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [fontSize, setFontSize] = useLocalStorage('fontSize', 16);

  return (
    <div>
      <select value={theme} onChange={(e) => setTheme(e.target.value)}>
        <option value="light">Açık</option>
        <option value="dark">Koyu</option>
      </select>
      <input
        type="range"
        min={12}
        max={24}
        value={fontSize}
        onChange={(e) => setFontSize(Number(e.target.value))}
      />
    </div>
  );
}

2. useFetch - Veri Çekme

import { useState, useEffect, useCallback } from 'react';

function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const fetchData = useCallback(async () => {
    const controller = new AbortController();

    try {
      setLoading(true);
      setError(null);

      const response = await fetch(url, {
        ...options,
        signal: controller.signal
      });

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      const result = await response.json();
      setData(result);
    } catch (err) {
      if (err.name !== 'AbortError') {
        setError(err.message);
      }
    } finally {
      setLoading(false);
    }

    return () => controller.abort();
  }, [url]);

  useEffect(() => {
    const cleanup = fetchData();
    return () => { cleanup?.then(fn => fn?.()); };
  }, [fetchData]);

  const refetch = () => fetchData();

  return { data, loading, error, refetch };
}

// Kullanım
function UserList() {
  const { data: users, loading, error, refetch } = useFetch('/api/users');

  if (loading) return <p>Yükleniyor...</p>;
  if (error) return <p>Hata: {error} <button onClick={refetch}>Tekrar Dene</button></p>;

  return (
    <ul>
      {users?.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

3. useDebounce - Geciktirilmiş Değer

function useDebounce(value, delay = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// Kullanım - arama input'u için
function SearchComponent() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 500);
  const { data: results } = useFetch(
    debouncedQuery ? `/api/search?q=${debouncedQuery}` : null
  );

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Arama..."
      />
      {results?.map(item => (
        <div key={item.id}>{item.title}</div>
      ))}
    </div>
  );
}

İpucu: Custom hook'lar birbirini çağırabilir. Yukarıdaki örnekte useDebounce ve useFetch birlikte kullanılarak güçlü bir arama deneyimi oluşturulmuştur. Hook'ları küçük ve odaklı tutup birleştirerek kullanmak en iyi pratiktir.

4. useMediaQuery - Responsive Tasarım

function useMediaQuery(query) {
  const [matches, setMatches] = useState(() => {
    if (typeof window === 'undefined') return false;
    return window.matchMedia(query).matches;
  });

  useEffect(() => {
    const mediaQuery = window.matchMedia(query);
    const handler = (event) => setMatches(event.matches);

    mediaQuery.addEventListener('change', handler);
    setMatches(mediaQuery.matches);

    return () => mediaQuery.removeEventListener('change', handler);
  }, [query]);

  return matches;
}

// Kullanım
function ResponsiveLayout() {
  const isMobile = useMediaQuery('(max-width: 768px)');
  const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
  const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');

  return (
    <div className={isMobile ? 'layout-mobile' : 'layout-desktop'}>
      {isMobile ? <MobileNav /> : <DesktopNav />}
      {prefersDark && <p>Koyu tema tercihiniz algılandı</p>}
    </div>
  );
}

5. useClickOutside - Dış Tıklama Algılama

function useClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) {
        return;
      }
      handler(event);
    };

    document.addEventListener('mousedown', listener);
    document.addEventListener('touchstart', listener);

    return () => {
      document.removeEventListener('mousedown', listener);
      document.removeEventListener('touchstart', listener);
    };
  }, [ref, handler]);
}

// Kullanım - Dropdown menu
function Dropdown({ items }) {
  const [isOpen, setIsOpen] = useState(false);
  const dropdownRef = useRef(null);

  useClickOutside(dropdownRef, () => setIsOpen(false));

  return (
    <div ref={dropdownRef} className="dropdown">
      <button onClick={() => setIsOpen(!isOpen)}>Menu</button>
      {isOpen && (
        <ul className="dropdown-menu">
          {items.map(item => (
            <li key={item.id} onClick={() => setIsOpen(false)}>{item.label}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

Uyarı: Custom hook'lar içinde yan etkilerin cleanup'ını yapmayı unutmayın. Event listener'lar, timer'lar ve AbortController'lar temizlenmezse memory leak'lere yol açar. useEffect'in return fonksiyonunu her zaman kullanın.

Custom Hook Test Etme

// useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter', () => {
  it('başlangıç değeri ile başlar', () => {
    const { result } = renderHook(() => useCounter(10));
    expect(result.current.count).toBe(10);
  });

  it('increment doğru çalışır', () => {
    const { result } = renderHook(() => useCounter(0));
    act(() => {
      result.current.increment();
    });
    expect(result.current.count).toBe(1);
  });
});

Sonuç

Custom hook'lar, React'te kod tekrarını azaltmanın ve mantığı paylaşmanın en zarif yoludur. useLocalStorage, useFetch, useDebounce gibi genel amaçlı hook'lardan uygulamanıza özel hook'lara kadar geniş bir yelpazede kullanabilirsiniz. Hook'larınızı küçük, odaklı ve test edilebilir tutun. Topluluk tarafından geliştirilen react-use ve usehooks-ts gibi kütüphaneler de hazır custom hook'lar sunar.

Kaynaklar

İlgili Yazılar