React'ta Custom Hook'lar ve Performans Optimizasyonu
React'ta custom hook'lar, uygulama mantığını yeniden kullanılabilir ve modüler parçalara ayırmak için kullanılan güçlü bir pattern'dir. Bu yazıda, custom hook'ların etkili kullanımını, yaygın kullanım senaryolarını ve performans optimizasyonu tekniklerini detaylı örneklerle inceleyeceğiz.
Custom Hook'lar Nedir ve Neden Kullanmalıyız?
Custom hook'lar, React'ın yerleşik hook'larını (useState, useEffect, useCallback vb.) kullanarak oluşturduğumuz özel hook'lardır. Bu hook'lar sayesinde:
- Kod Tekrarını Azaltma: Birden fazla bileşende kullanılan mantığı tek bir yerde toplayarak DRY (Don't Repeat Yourself) prensibini uygulayabilirsiniz.
- Mantık Modülerliği: Karmaşık iş mantığını bileşenlerden ayırarak daha temiz ve anlaşılır bir kod yapısı oluşturabilirsiniz.
- Test Edilebilirlik: İş mantığını izole edilmiş hook'larda tutarak, test yazımını kolaylaştırır ve test coverage'ını artırabilirsiniz.
- Bileşen Karmaşıklığını Azaltma: Bileşenleri daha sade ve anlaşılır hale getirerek, bakım maliyetini düşürebilirsiniz.
- Kod Organizasyonu: İlgili mantıkları bir arada tutarak, kodun organizasyonunu ve okunabilirliğini artırabilirsiniz.
Temel Custom Hook Örneği: useLocalStorage
Local storage işlemlerini yönetmek için kullanılan bu hook, tarayıcı storage'ında veri persistance işlemlerini kolaylaştırır ve yaygın hataları önler.
import { useState, useEffect } from 'react'; function useLocalStorage<T>(key: string, initialValue: T) { // State'i başlat // localStorage'dan veriyi okuma ve parse etme işlemi // initialValue bir fonksiyon da olabilir (lazy initialization) const [value, setValue] = useState<T>(() => { try { const item = window.localStorage.getItem(key); // Eğer item varsa JSON parse et, yoksa initial value'yu kullan return item ? JSON.parse(item) : initialValue; } catch (error) { // JSON parse hatası veya localStorage erişim hatası durumunda console.error('Local storage error:', error); return initialValue; } }); // Local storage'ı güncelle // value veya key değiştiğinde storage'ı otomatik güncelle useEffect(() => { try { // Değeri JSON string'e çevir ve storage'a kaydet window.localStorage.setItem(key, JSON.stringify(value)); } catch (error) { // Storage limiti aşıldığında veya erişim hatası durumunda console.error('Local storage error:', error); } }, [key, value]); // Tuple type ile dönüş yaparak, array destructuring kullanımını sağla return [value, setValue] as const; } // Kullanım örneği function App() { // Theme state'ini local storage'da tut // Sayfa yenilendiğinde bile theme değeri korunur const [theme, setTheme] = useLocalStorage('theme', 'light'); return ( <div className={`app ${theme}`}> <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')} className="theme-toggle" > {theme === 'light' ? '🌙' : '☀️'} Toggle Theme </button> </div> ); }
Performans Odaklı Custom Hook: useDebounce
Kullanıcı girdilerini optimize etmek ve gereksiz API çağrılarını önlemek için kullanılan bu hook, özellikle arama işlemlerinde performansı artırır.
import { useState, useEffect } from 'react'; function useDebounce<T>(value: T, delay: number): T { // Debounce edilmiş değeri tut const [debouncedValue, setDebouncedValue] = useState<T>(value); useEffect(() => { // Yeni bir timer oluştur const timer = setTimeout(() => { // Delay süresi sonunda değeri güncelle setDebouncedValue(value); }, delay); // Cleanup: Yeni bir değer geldiğinde önceki timer'ı temizle // Bu sayede art arda gelen değişikliklerde sadece son değer işlenir return () => { clearTimeout(timer); }; }, [value, delay]); // value veya delay değiştiğinde effect'i tekrar çalıştır return debouncedValue; } // Kullanım örneği - Arama Komponenti function SearchComponent() { // Anlık arama değeri const [search, setSearch] = useState(''); // 500ms debounce edilmiş arama değeri const debouncedSearch = useDebounce(search, 500); // Debounce edilmiş değer değiştiğinde API çağrısı yap useEffect(() => { if (debouncedSearch) { // API çağrısı yap fetchSearchResults(debouncedSearch).then(results => { // Sonuçları işle }); } }, [debouncedSearch]); return ( <div className="search-container"> <input type="text" value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Ara..." className="search-input" /> {/* Yükleme durumu ve sonuçları göster */} </div> ); }
Performans Optimizasyonu Teknikleri
Modern React uygulamalarında performans optimizasyonu, kullanıcı deneyimini doğrudan etkileyen kritik bir konudur. İşte en etkili optimizasyon teknikleri ve bunların custom hook'lar ile implementasyonu:
1. useMemo ve useCallback ile Memoization
Memoization, pahalı hesaplamaların sonuçlarını cache'leyerek gereksiz yeniden hesaplamaları önleyen bir optimizasyon tekniğidir. React'ta useMemo ve useCallback hook'ları bu amaçla kullanılır.
import { useMemo, useCallback, useState } from 'react'; interface Item { id: string; data: number[]; timestamp: Date; } interface Props { data: Item[]; onItemSelect: (id: string) => void; threshold: number; } function ExpensiveComponent({ data, onItemSelect, threshold }: Props) { // Pahalı veri işleme ve filtreleme const processedData = useMemo(() => { console.log('Expensive calculation running...'); // Debug için return data .filter(item => { // Karmaşık filtreleme işlemi const average = item.data.reduce((sum, num) => sum + num, 0) / item.data.length; return average > threshold; }) .map(item => ({ ...item, processed: item.data.map(num => num * 2), // Veri transformasyonu lastUpdated: item.timestamp.toLocaleDateString() })) .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); // Sıralama }, [data, threshold]); // Sadece data veya threshold değiştiğinde yeniden hesapla // Event handler memoization - gereksiz re-render'ları önler const handleSelect = useCallback((id: string) => { // Ek işlemler... onItemSelect(id); }, [onItemSelect]); return ( <div className="data-grid"> {processedData.map(item => ( <div key={item.id} className="data-item" onClick={() => handleSelect(item.id)} > <h3>Item {item.id}</h3> <div>Processed Values: {item.processed.join(', ')}</div> <div>Last Updated: {item.lastUpdated}</div> </div> ))} </div> ); }
2. Custom Hook ile Intersection Observer
Intersection Observer API'yi kullanarak lazy loading ve sonsuz scroll gibi performans optimizasyonlarını kolaylaştıran bir custom hook implementasyonu:
interface IntersectionOptions extends IntersectionObserverInit { freezeOnceVisible?: boolean; } function useIntersectionObserver( ref: React.RefObject<Element>, options: IntersectionOptions = {} ) { const { threshold = 0, root = null, rootMargin = '0px', freezeOnceVisible = false } = options; const [entry, setEntry] = useState<IntersectionObserverEntry>(); const frozen = entry?.isIntersecting && freezeOnceVisible; useEffect(() => { const node = ref.current; if (!node || frozen) return; // Callback fonksiyonu const updateEntry = ([entry]: IntersectionObserverEntry[]): void => { setEntry(entry); }; // Observer'ı yapılandır ve başlat const observer = new IntersectionObserver(updateEntry, { threshold, root, rootMargin }); observer.observe(node); // Cleanup return () => { observer.disconnect(); }; }, [ref, threshold, root, rootMargin, frozen]); return entry; } // Gelişmiş Lazy Image komponenti örneği interface LazyImageProps { src: string; alt: string; width?: number; height?: number; className?: string; loadingComponent?: React.ReactNode; errorComponent?: React.ReactNode; } function LazyImage({ src, alt, width, height, className, loadingComponent = <div>Loading...</div>, errorComponent = <div>Error loading image</div> }: LazyImageProps) { const ref = useRef<HTMLDivElement>(null); const entry = useIntersectionObserver(ref, { freezeOnceVisible: true, rootMargin: '50px' // Pre-loading için margin }); const [isLoaded, setIsLoaded] = useState(false); const [hasError, setHasError] = useState(false); const isVisible = entry?.isIntersecting; return ( <div ref={ref} className={`lazy-image-container ${className || ''}`} style={{ width, height }} > {isVisible && !hasError ? ( <img src={src} alt={alt} className={`lazy-image ${isLoaded ? 'loaded' : ''}`} onLoad={() => setIsLoaded(true)} onError={() => setHasError(true)} loading="lazy" /> ) : null} {isVisible && !isLoaded && !hasError && loadingComponent} {hasError && errorComponent} </div> ); }
3. Form Yönetimi için Custom Hook
function useForm<T extends Record<string, any>>(initialValues: T) { const [values, setValues] = useState<T>(initialValues); const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({}); const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({}); const handleChange = useCallback(( e: React.ChangeEvent<HTMLInputElement> ) => { const { name, value } = e.target; setValues(prev => ({ ...prev, [name]: value })); }, []); const handleBlur = useCallback(( e: React.FocusEvent<HTMLInputElement> ) => { const { name } = e.target; setTouched(prev => ({ ...prev, [name]: true })); }, []); const reset = useCallback(() => { setValues(initialValues); setErrors({}); setTouched({}); }, [initialValues]); return { values, errors, touched, handleChange, handleBlur, reset }; } // Kullanım örneği function LoginForm() { const { values, errors, touched, handleChange, handleBlur, reset } = useForm({ email: '', password: '' }); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); // Form gönderme işlemi reset(); }; return ( <form onSubmit={handleSubmit}> <input type="email" name="email" value={values.email} onChange={handleChange} onBlur={handleBlur} /> {touched.email && errors.email && ( <span>{errors.email}</span> )} {/* Diğer form alanları */} </form> ); }
4. Network İstekleri için Custom Hook
function useApi<T>(url: string) { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState(false); const [error, setError] = useState<Error | null>(null); const fetchData = useCallback(async () => { setLoading(true); try { const response = await fetch(url); if (!response.ok) { throw new Error('Network response was not ok'); } const json = await response.json(); setData(json); setError(null); } catch (err) { setError(err instanceof Error ? err : new Error('An error occurred')); setData(null); } finally { setLoading(false); } }, [url]); useEffect(() => { fetchData(); }, [fetchData]); const refetch = useCallback(() => { fetchData(); }, [fetchData]); return { data, loading, error, refetch }; } // Kullanım örneği function UserProfile({ userId }: { userId: string }) { const { data: user, loading, error } = useApi<User>( `/api/users/${userId}` ); if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>; if (!user) return null; return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> ); }
Best Practices ve İpuçları
Hook Kurallarına Uyun
- Hook'ları her zaman fonksiyonun en üst seviyesinde çağırın
- Hook'ları sadece React fonksiyon bileşenlerinde kullanın
- İsimlendirmede "use" prefix'ini kullanın
Dependency Array'leri Doğru Yönetin
// Kötü useEffect(() => { // Her render'da çalışır }); // İyi useEffect(() => { // Sadece dependencies değiştiğinde çalışır }, [dep1, dep2]);
TypeScript ile Tip Güvenliği
function useCounter<T extends number>( initialValue: T ): [T, () => void, () => void] { const [count, setCount] = useState<T>(initialValue); const increment = () => setCount((prev: T) => (prev + 1) as T); const decrement = () => setCount((prev: T) => (prev - 1) as T); return [count, increment, decrement]; }
Cleanup İşlemlerini Unutmayın
useEffect(() => { const subscription = subscribe(); return () => { subscription.unsubscribe(); }; }, []);
Sonuç
Custom hook'lar ve performans optimizasyonu, React uygulamalarının kalitesini ve bakımını önemli ölçüde iyileştirir. Önemli noktalar:
- Custom hook'ları modüler ve yeniden kullanılabilir tasarlayın
- Performans optimizasyonlarını erken yapmaktan kaçının
- TypeScript ile tip güvenliği sağlayın
- Hook kurallarına ve best practice'lere uyun
- Cleanup işlemlerini ihmal etmeyin
İlgili Etiketler: #React #CustomHooks #Performance #TypeScript #WebDevelopment #Frontend #JavaScript