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:
- Her zaman prop ve state'in en güncel değerlerini okur (stale closure olmaz)
- useEffect'in dependency array'inde yer almaz
- Reaktif değildir — effect'in ne zaman çalışacağını etkilemez
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:
roomIddeğişti → yeniden bağlanurldeğişti → yeniden fetch et
Effect Event'leri
Effect çalıştığında ne yapılacağını belirleyen ama çalışma zamanını etkilemeyen değerler:
- Bağlanınca hangi
themeile bildirim göster - Fetch tamamlanınca hangi
onSuccesscallback'ini çağır
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):
useEffectEventhâ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 roomIduseRef ile Karşılaştırma
useRef yaklaşımı çalışır ama:
- Fazla boilerplate kod gerektirir
- Her değer için ayrı ref + effect yazmanız gerekir
- Hata yapma riski yüksektir
- React'in optimizasyonlarından yararlanamaz
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:
- Sorun:
useEffectdependency array'ine eklenen her değer effect'i yeniden tetikler, ancak bazı değerler sadece okunması gereken "yan bilgilerdir." - Çözüm:
useEffectEvent, reaktif olmayan mantığı effect'ten ayırarak bu değerlerin her zaman güncel okunmasını sağlarken effect'in çalışma zamanını etkilemesini önler. - Faydaları: Stale closure problemini çözer, gereksiz yeniden çalışmaları önler,
eslint-disableihtiyacını ortadan kaldırır ve kodu çok daha okunabilir hale getirir. - Mevcut durum: Henüz deneysel aşamada olsa da arkasındaki zihinsel model bugün bile kodunuzun kalitesini artırabilir.
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.