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