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ı
useile 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.