İçeriğe geç
react

useEffect Hook'u: Yan Etki Yönetiminin Tam Rehberi

Tarık Tunç
useEffect Hook'u: Yan Etki Yönetiminin Tam Rehberi

useEffect Hook'u: Yan Etki Yönetiminin Tam Rehberi

React bileşenlerinin ana görevi UI render etmektir. Ancak gerçek dünya uygulamalarında API çağrıları, event listener'lar, zamanlayıcılar ve DOM manipülasyonları gibi "yan etkiler" (side effects) kaçınılmazdır. useEffect hook'u, bu yan etkileri güvenli ve kontrollü bir şekilde yönetmenizi sağlar. Bu rehberde useEffect'in çalışma mekanizmasını, dependency array'in inceliklerini ve yaygın tuzakları detaylı olarak ele alacağız.

useEffect'in Temel Yapısı

useEffect üç parçadan oluşur: setup fonksiyonu, cleanup fonksiyonu ve dependency array:

import { useEffect, useState } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 1. SETUP: Yan etki kodu
    const controller = new AbortController();

    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`, {
          signal: controller.signal
        });
        const data = await response.json();
        setUser(data);
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('Kullanıcı çekilemedi:', error);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchUser();

    // 2. CLEANUP: Temizleme fonksiyonu
    return () => {
      controller.abort();
    };
  }, [userId]); // 3. DEPENDENCY ARRAY: Bağımlılıklar

  if (loading) return <p>Yükleniyor...</p>;
  if (!user) return <p>Kullanıcı bulunamadı</p>;

  return <div><h2>{user.name}</h2><p>{user.email}</p></div>;
}

Dependency Array Davranışları

Dependency array, useEffect'in ne zaman çalışacağını belirler:

// 1. Dependency array YOK - her render'da çalışır
useEffect(() => {
  console.log('Her render sonrası çalışır');
});

// 2. Boş dependency array - yalnızca mount'ta çalışır
useEffect(() => {
  console.log('Yalnızca ilk render sonrası çalışır');
  return () => console.log('Unmount olduğunda çalışır');
}, []);

// 3. Bağımlılıklarla - belirtilen değerler değiştiğinde çalışır
useEffect(() => {
  console.log('userId veya role değiştiğinde çalışır');
  return () => console.log('Yeni effect öncesi ve unmount\'ta çalışır');
}, [userId, role]);

Uyarı: Dependency array'i boş bırakmak ([]) ile hiç vermemek arasında büyük fark vardır. Boş array "yalnızca mount/unmount'ta çalış" anlamına gelirken, array'i hiç vermemek "her render'da çalış" anlamına gelir. Bu farkı gözden kaçırmak sonsuz döngülere neden olabilir.

Cleanup Fonksiyonu: Neden Önemli?

Cleanup fonksiyonu, memory leak'leri ve stale state sorunlarını önler. Şu durumlarda mutlaka kullanılmalıdır:

function LiveChat({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    // WebSocket bağlantısı
    const ws = new WebSocket(`wss://chat.example.com/rooms/${roomId}`);

    ws.onmessage = (event) => {
      const newMessage = JSON.parse(event.data);
      setMessages(prev => [...prev, newMessage]);
    };

    ws.onerror = (error) => {
      console.error('WebSocket hatası:', error);
    };

    // Cleanup: Bağlantıyı kapat
    return () => {
      ws.close();
    };
  }, [roomId]);

  return (
    <div>
      {messages.map((msg, i) => (
        <p key={i}><strong>{msg.user}:</strong> {msg.text}</p>
      ))}
    </div>
  );
}

function WindowResize() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    // Event listener ekleme
    window.addEventListener('resize', handleResize);

    // Cleanup: Event listener'ı kaldırma
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return <p>Pencere: {windowSize.width} x {windowSize.height}</p>;
}

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    // Interval başlatma
    const intervalId = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    // Cleanup: Interval'i temizleme
    return () => {
      clearInterval(intervalId);
    };
  }, []);

  return <p>Süre: {seconds} saniye</p>;
}

Race Condition Sorunu ve Çözümü

Ardışık API çağrılarında önceki çağrının sonucunun sonraki çağrının sonucunu ezmesi race condition sorununa yol açar:

function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    // Race condition'ı önlemek için flag kullanımı
    let isCancelled = false;

    const search = async () => {
      if (!query) {
        setResults([]);
        return;
      }

      try {
        const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
        const data = await response.json();

        // Effect temizlendiyse sonucu uygulamayı
        if (!isCancelled) {
          setResults(data.results);
        }
      } catch (error) {
        if (!isCancelled) {
          console.error('Arama hatası:', error);
        }
      }
    };

    search();

    return () => {
      isCancelled = true; // Önceki arama sonucunu yoksay
    };
  }, [query]);

  return (
    <ul>
      {results.map(result => (
        <li key={result.id}>{result.title}</li>
      ))}
    </ul>
  );
}

İpucu: Modern projelerde API çağrıları için useEffect yerine TanStack Query (React Query) veya SWR gibi kütüphaneler kullanmayı değerlendirin. Bu kütüphaneler caching, retry, race condition yönetimi ve daha fazlasını otomatik olarak yönetir.

useEffect'te Yapılan Yaygın Hatalar

1. Sonsuz Döngü

function BadExample() {
  const [data, setData] = useState([]);

  // HATALI: Her render'da yeni object/array oluşturulur
  // Bu da useEffect'in sürekli çalışmasına neden olur
  useEffect(() => {
    fetch('/api/data')
      .then(res => res.json())
      .then(data => setData(data));
  }); // dependency array yok = her render'da çalışır!

  // HATALI: Object dependency
  const options = { page: 1, limit: 10 }; // Her render'da yeni referans
  useEffect(() => {
    fetchData(options);
  }, [options]); // Sonsuz döngü!

  // DOĞRU: Değerleri ayrı ayrı bağımlılık yapın
  const page = 1;
  const limit = 10;
  useEffect(() => {
    fetchData({ page, limit });
  }, [page, limit]);
}

2. Async Fonksiyon Doğrudan Kullanımı

// HATALI: useEffect callback'i doğrudan async olamaz
useEffect(async () => {
  const data = await fetchData(); // Bu Promise döndürür, cleanup değil
  setData(data);
}, []);

// DOĞRU: İç fonksiyon tanımlayın
useEffect(() => {
  const loadData = async () => {
    const data = await fetchData();
    setData(data);
  };
  loadData();
}, []);

Bilgi: useEffect callback'i ya undefined ya da bir cleanup fonksiyonu döndürmelidir. Async fonksiyonlar her zaman Promise döndürdüğü için doğrudan useEffect callback'i olarak kullanılamazlar.

useEffect Çalışma Sırası

function LifecycleDemo() {
  console.log('1. Render (fonksiyon gövdesi çalışır)');

  useEffect(() => {
    console.log('3. Effect çalışır (render sonrası)');
    return () => {
      console.log('2. Cleanup çalışır (yeni effect öncesi veya unmount)');
    };
  });

  return <div>{console.log('2. JSX oluşturulur')}Demo</div>;
}

// İlk render: 1 → 2 → 3
// Sonraki renderlar: 1 → 2 → Cleanup → 3
// Unmount: Cleanup

Sonuç

useEffect, React'te yan etki yönetiminin temel aracıdır. Dependency array'in doğru kullanımı, cleanup fonksiyonlarının yazılması ve race condition'ların önlenmesi en kritik konulardır. Her useEffect kullandığınızda kendinize "Bu effect'in cleanup'a ihtiyacı var mı?" ve "Dependency array'im doğru mu?" sorularını sorun. Gereksiz useEffect kullanımından kaçının; çoğu hesaplama doğrudan render sırasında veya event handler'larda yapılabilir.

Kaynaklar

İlgili Yazılar