React'ta Custom Hook'lar ve Performans Optimizasyonu

Yunus Emre Güzel
9 Ocak 202515 dkReact
React'ta Custom Hook'lar ve Performans Optimizasyonu

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ı

  1. 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
  2. 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]);
    
  3. 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];
    }
    
  4. 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

Kaynaklar