İçeriğe geç
Sedat Demir
Geri dön

useEffectEvent Hook: useEffect Dependency Array Sorununa Köklü Çözüm

useEffectEvent Hook: useEffect Dependency Array Sorununa Köklü Çözüm

React geliştiricilerinin büyük çoğunluğu bu senaryoyu yaşamıştır: useEffect içinde bir fonksiyon çağırıyorsunuz, dependency array'e eklemeniz gereken değerler sürekli artıyor, her eklediğiniz dependency yeni bir yeniden çalışmaya neden oluyor ve sonunda ya lint kurallarını bastırıyorsunuz ya da kodunuz beklediğiniz gibi çalışmıyor. İşte useEffectEvent tam olarak bu kısır döngüyü kırmak için tasarlandı.

Bu yazıda, useEffectEvent hook'unun ne olduğunu, hangi sorunu çözdüğünü ve gerçek dünya senaryolarında nasıl kullanılacağını derinlemesine inceleyeceğiz.


Sorunun Kökeni: useEffect ve Dependency Array

Klasik Senaryo

Bir sohbet uygulaması düşünün. Kullanıcı bir odaya bağlandığında sunucuya bağlanmak ve bağlantı kurulduğunda bir bildirim göstermek istiyorsunuz:

function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.on('connected', () => {
      showNotification('Bağlandı!', theme);
    });
    connection.connect();

    return () => connection.disconnect();
  }, [roomId, theme]); // 🤔 theme değişince tekrar bağlanmak zorunda mıyız?

  return <div>Sohbet Odası: {roomId}</div>;
}

Buradaki sorun çok net: theme değiştiğinde effect yeniden çalışıyor, yani sunucu bağlantısı kesiliyor ve yeniden kuruluyor. Oysa biz sadece roomId değiştiğinde yeniden bağlanmak istiyoruz. theme sadece bildirim gösterilirken lazım.

Geleneksel (ve Sorunlu) Çözüm Denemeleri

Deneme 1: theme'i dependency'den çıkarmak

useEffect(() => {
  const connection = createConnection(roomId);
  connection.on('connected', () => {
    showNotification('Bağlandı!', theme);
  });
  connection.connect();
  return () => connection.disconnect();
}, [roomId]); // ⛔ ESLint: React Hook useEffect has a missing dependency: 'theme'

Bu çalışır gibi görünse de stale closure tuzağına düşersiniz. theme değiştiğinde eski değeri kullanılmaya devam eder. ESLint de sizi haklı olarak uyarır.

Deneme 2: eslint-disable kullanmak

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [roomId]);

Bu yaklaşım sadece sorunu halının altına süpürür. Gelecekte yeni dependency'ler eklendiğinde fark edilmeyen buglar oluşur. Asla önerilmez.

Deneme 3: useRef ile son değeri takip etmek

function ChatRoom({ roomId, theme }) {
  const themeRef = useRef(theme);

  useEffect(() => {
    themeRef.current = theme;
  }, [theme]);

  useEffect(() => {
    const connection = createConnection(roomId);
    connection.on('connected', () => {
      showNotification('Bağlandı!', themeRef.current);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return <div>Sohbet Odası: {roomId}</div>;
}

Bu işe yarar ama boilerplate kod çok fazladır. Her böyle değer için aynı pattern'i tekrarlamanız gerekir. Ayrıca hataya açıktır.


useEffectEvent Nedir?

useEffectEvent, React ekibinin bu soruna sunduğu resmi ve köklü çözümdür. Temel fikir şudur:

Effect'lerinizin içinde "reaktif olmayan" mantık parçaları vardır. Bu parçalar her zaman en güncel değerleri okumalı ama dependency olarak sayılmamalıdır.

useEffectEvent, bir Effect Event tanımlamanızı sağlar. Bu event:

Temel Sözdizimi

import { useEffectEvent } from 'react';

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Bağlandı!', theme);
  });

  useEffect(() => {
    const connection = createConnection(roomId);
    connection.on('connected', () => {
      onConnected();
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ✅ theme dependency olarak gerekmiyor!

  return <div>Sohbet Odası: {roomId}</div>;
}

Dikkat edin: onConnected fonksiyonu theme'i kullanıyor ama dependency array'de theme yok. Ve bu tamamen güvenli çünkü useEffectEvent her çağrıldığında theme'in o anki en güncel değerini okur.


Reaktif Değerler vs. Event'ler: Kavramsal Ayrım

useEffectEvent'i doğru anlamak için React'in yeni zihinsel modelini kavramak gerekir.

Reaktif Değerler

Effect'in neden çalıştığını belirleyen değerler:

Effect Event'leri

Effect çalıştığında ne yapılacağını belirleyen ama çalışma zamanını etkilemeyen değerler:

Bu ayrım şöyle düşünülebilir:

Özellik Reaktif Değer Effect Event
Değişince effect tetiklenir mi? ✅ Evet ❌ Hayır
En güncel değeri okur mu? ✅ Evet ✅ Evet
Dependency array'de yer alır mı? ✅ Evet ❌ Hayır

Gerçek Dünya Örnekleri

Örnek 1: Sayfa Görüntüleme Analitik Takibi

function ProductPage({ productId, category, analyticsConfig }) {
  const onPageView = useEffectEvent(() => {
    // analyticsConfig her render'da değişebilir
    // ama sayfa görüntüleme sadece productId değişince kaydedilmeli
    sendAnalytics('page_view', {
      productId,
      category,
      ...analyticsConfig,
      timestamp: Date.now(),
    });
  });

  useEffect(() => {
    onPageView();
  }, [productId]); // ✅ Sadece productId değişince analitik gönder

  return <ProductDetails id={productId} />;
}

Burada analyticsConfig objesi muhtemelen her render'da yeni bir referans oluşturuyor. Eski yöntemle bu objeyi dependency array'e eklemek, her render'da analitik event'i göndermek anlamına gelirdi. useEffectEvent sayesinde analitik sadece productId değiştiğinde gönderiliyor ama gönderildiğinde en güncel analyticsConfig değerleri kullanılıyor.

Örnek 2: WebSocket Bağlantısıyla Bildirim Yönetimi

function NotificationCenter({ userId, notificationPreferences }) {
  const onNewNotification = useEffectEvent((notification) => {
    // notificationPreferences reaktif ama
    // WebSocket bağlantısını yeniden kurmak istemiyoruz
    if (notificationPreferences.sound) {
      playSound(notificationPreferences.soundType);
    }

    if (notificationPreferences.desktop) {
      showDesktopNotification(notification.title, notification.body);
    }

    if (notificationPreferences.badge) {
      updateBadgeCount((prev) => prev + 1);
    }
  });

  useEffect(() => {
    const ws = new WebSocket(`wss://api.example.com/notifications/${userId}`);

    ws.onmessage = (event) => {
      const notification = JSON.parse(event.data);
      onNewNotification(notification);
    };

    return () => ws.close();
  }, [userId]); // ✅ Sadece userId değişince yeni WebSocket bağlantısı

  return <NotificationList userId={userId} />;
}

Bu örnek çok güçlü bir kullanım senaryosunu gösteriyor. Kullanıcı bildirim tercihlerini değiştirdiğinde (ses aç/kapat, masaüstü bildirimleri aç/kapat) WebSocket bağlantısının kesilip yeniden kurulmasını istemiyoruz. Ama yeni bir bildirim geldiğinde en güncel tercihleri kullanmak istiyoruz.

Örnek 3: Debounced Arama ile Callback

function SearchComponent({ onResultsChange, locale }) {
  const [query, setQuery] = useState('');

  const onSearchComplete = useEffectEvent((results) => {
    // locale ve onResultsChange her render'da değişebilir
    const formattedResults = results.map((r) => ({
      ...r,
      title: formatForLocale(r.title, locale),
    }));
    onResultsChange(formattedResults);
  });

  useEffect(() => {
    if (!query) return;

    const controller = new AbortController();
    const timeoutId = setTimeout(async () => {
      try {
        const results = await searchAPI(query, {
          signal: controller.signal,
        });
        onSearchComplete(results);
      } catch (err) {
        if (!controller.signal.aborted) {
          console.error('Arama hatası:', err);
        }
      }
    }, 300);

    return () => {
      clearTimeout(timeoutId);
      controller.abort();
    };
  }, [query]); // ✅ Sadece query değişince arama yap

  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Ara..."
    />
  );
}

Burada onResultsChange prop olarak gelen bir callback. Parent component her render'da yeni bir fonksiyon referansı gönderebilir. locale de her an değişebilir. İkisini de dependency array'e eklersek her render'da arama tetiklenir. useEffectEvent bu sorunu tamamen ortadan kaldırıyor.


useEffectEvent Kuralları ve Kısıtlamaları

useEffectEvent kullanırken dikkat etmeniz gereken önemli kurallar vardır:

1. Sadece useEffect İçinden Çağırın

// ✅ Doğru
useEffect(() => {
  onSomething();
}, [dependency]);

// ❌ Yanlış — event handler'dan çağırmayın
function handleClick() {
  onSomething(); // Bu amaçla tasarlanmadı
}

2. Başka Bileşenlere Prop Olarak Geçirmeyin

// ❌ Yanlış
const onTick = useEffectEvent(() => { /* ... */ });
return <Timer onTick={onTick} />; // Bunu yapmayın!

3. Her Effect Event'i Kullanıldığı Effect'e Yakın Tanımlayın

Kodun okunabilirliği için effect event'lerini ilgili useEffect'e yakın yerde tanımlayın.

4. Effect Event İçinden State Güncellemesi Yapabilirsiniz

const onMessage = useEffectEvent((message) => {
  setMessages((prev) => [...prev, message]); // ✅ Güvenli
  if (notificationsEnabled) {                 // ✅ En güncel değer
    showNotification(message.text);
  }
});

Mevcut Durum ve Kullanıma Hazırlık

Önemli Not (2024): useEffectEvent hâlâ deneysel (experimental) bir API'dir. React'in kararlı sürümlerinde henüz mevcut değildir. Kullanmak için React'in canary veya experimental sürümlerini kullanmanız gerekir.

npm install react@experimental react-dom@experimental
// Experimental sürümde import
import { experimental_useEffectEvent as useEffectEvent } from 'react';

Ancak bu hook'un arkasındaki zihinsel model şu anda bile kodunuzu yapılandırmanızda size rehberlik edebilir. Bugün bu pattern'i useRef ile manuel olarak uygulayabilirsiniz:

// useEffectEvent'in polyfill benzeri implementasyonu
function useEffectEventPolyfill(fn) {
  const ref = useRef(fn);

  useLayoutEffect(() => {
    ref.current = fn;
  });

  return useCallback((...args) => {
    return ref.current(...args);
  }, []);
}

Bu polyfill tam olarak aynı garantileri sağlamasa da birçok kullanım senaryosunda benzer şekilde çalışır.


useEffectEvent vs. Alternatif Yaklaşımlar

useCallback ile Karşılaştırma

// useCallback — dependency yönetimi hâlâ gerekli
const handleConnected = useCallback(() => {
  showNotification('Bağlandı!', theme);
}, [theme]); // theme değişince fonksiyon yenilenir → effect yeniden çalışır

useEffect(() => {
  const connection = createConnection(roomId);
  connection.on('connected', handleConnected);
  connection.connect();
  return () => connection.disconnect();
}, [roomId, handleConnected]); // ❌ handleConnected dependency

// useEffectEvent — temiz ve doğru çözüm
const onConnected = useEffectEvent(() => {
  showNotification('Bağlandı!', theme);
});

useEffect(() => {
  const connection = createConnection(roomId);
  connection.on('connected', onConnected);
  connection.connect();
  return () => connection.disconnect();
}, [roomId]); // ✅ Sadece roomId

useRef ile Karşılaştırma

useRef yaklaşımı çalışır ama:

useEffectEvent tüm bunları tek satırda çözer.


Sonuç

useEffectEvent, React'in hook ekosistemindeki en önemli eksiklerden birini dolduruyor. Dependency array'in "ya hep ya hiç" yaklaşımının yarattığı sorunlara kavramsal düzeyde doğru bir çözüm sunuyor.

Özetle:

React ekibinin bu hook'u kararlı sürüme taşıması, dependency array ile ilgili yaşanan binlerce soruna son noktayı koyacak. O zamana kadar bu kavramları öğrenmek ve polyfill yaklaşımlarını benimsemek, sizi bir adım öne taşıyacaktır.


Share this post on:

Sonraki Yazı
Clerk ile Authentication: 10 Dakikada Kullanıcı Yönetimi