همانطور که میدانید، مدیریتکنندههای رویداد (Event Handlers) فقط زمانی دوباره اجرا میشوند که کاربر همان تعامل (مثل کلیک یا تایپ) را مجدداً تکرار کند. در مقابل، افکتها (Effects) کاملاً واکنشپذیر هستند و به محض تغییر مقادیری که میخوانند، فرآیند همگامسازی را از نو آغاز میکنند.
اما گاهی اوقات به ترکیبی از هر دو رفتار نیاز داریم: میخواهیم یک افکت در پاسخ به تغییرِ برخی از متغیرها اجرا شود، اما نسبت به تغییر متغیرهای دیگر کاملاً بیتفاوت باشد و فقط آخرین مقدار آنها را بخواند.
چطور مرز دقیق میان کدهای رویدادی و افکت را مشخص کنیم؟
چرا افکتها واکنشپذیرند اما رویدادها اینطور نیستند؟
هوک useEffectEvent چیست و چگونه منطق غیرواکنشپذیر را از دل افکت استخراج میکند؟
محدودیتهای استفاده از Effect Events چیست؟
بیایید برای درک بهتر، سناریوی یک اتاق گفتگو با دو نیازمندی مجزا را بررسی کنیم:
کامپوننت باید به صورت خودکار به اتاق انتخابشده متصل شود.
با کلیک روی دکمه «Send»، پیام کاربر ارسال شود.
برای تصمیمگیری درباره اینکه کدهای هر بخش را کجا بنویسیم، همیشه بپرسید: «این کد چرا و چه زمانی باید اجرا شود؟»
ارسال پیام باید فقط و فقط زمانی رخ دهد که کاربر روی دکمه کلیک میکند. اگر این کد در هر زمان دیگری اجرا شود، کاربر به شدت ناراضی خواهد بود! پس جایش دقیقاً در یک Event Handler است:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function handleSendClick() {
sendMessage(message); // 🟢 فقط با کلیک اجرا میشود
}
return (
<>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button onClick={handleSendClick}>Send</button>
</>
);
}
اتصال به سرور چت به هیچ دکمه خاصی وابسته نیست. فرقی نمیکند کاربر چطور به این صفحه هدایت شده است؛ همین که صفحه جلوی چشمان اوست، کامپوننت باید متصل بماند. بنابراین این منطق متعلق به یک Effect است:
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // 🔄 با تغییر اتاق، اتصال مجدداً همگامسازی میشود
فرض کنید میخواهید به محض برقراری اتصال چت، یک اعلان (Notification) به کاربر نشان دهید. برای اینکه رنگ اعلان با تم انتخابی کاربر (تاریک یا روشن) هماهنگ باشد، پروپ theme را درون افکت میخوانید:
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('متصل شد!', theme);
});
connection.connect();
return () => connection.disconnect();
}, [roomId, theme]); // 🚩 متغیر theme به ناچار وارد وابستگیها میشود
}
مشکل بزرگ این کد چیست؟ از آنجا که theme یک مقدار واکنشپذیر است، باید در آرایه وابستگیها ذکر شود. در نتیجه، هر بار که کاربر تم سایت را بین حالت شب و روز جابجا کند، اتصال سوکت چت قطع شده و دوباره برقرار میشود! این یک تجربه کاربری (UX) بسیار ضعیف است.
ما میخواهیم افکت ما نسبت به تغییرات theme واکنش نشان ندهد، اما زمانهایی که به خاطر تغییر roomId اجرا میشود، به آخرین مقدار theme دسترسی داشته باشد.
برای حل این پارادوکس، ریآکت هوک ویژهای به نام useEffectEvent را معرفی کرده است. این هوک به شما اجازه میدهد تکهای از کدهای درون افکت را که مایلید «غیرواکنشپذیر» باشند، بیرون بکشید:
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
// 💡 تعریف یک Effect Event
const onConnected = useEffectEvent(() => {
showNotification('متصل شد!', theme); // همیشه آخرین مقدار theme را میبیند
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected(); // فراخوانی امن بدون ایجاد وابستگی
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ دیگر نیازی به ذکر theme در آرایه وابستگی نیست!
}
توابع ساخته شده با useEffectEvent شباهت زیادی به مدیریتکنندههای رویداد دارند؛ با این تفاوت که به جای اکشن کاربر، توسط خودِ افکتها صدا زده میشوند. این تکنیک زنجیره واکنشپذیریِ افکت را برای کدهایی که نباید واکنشپذیر باشند، قطع میکند.
بیایید سناریوی ثبت آمار بازدید از صفحات (logVisit) را بررسی کنیم. میخواهیم با هر بار تغییر آدرس صفحه (url)، آمار ثبت شود؛ همچنین میخواهیم تعداد آیتمهای سبد خرید کاربر (numberOfItems) را هم در کنار آمار بفرستیم:
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
// استخراج بخش غیرواکنشپذیر به همراه آرگومان ورودی
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ کاملاً درست؛ تغییر تعداد آیتمهای سبد خرید، افکت را دوباره اجرا نمیکند
}
url را به عنوان آرگومان پاس دهیم؟شاید بپرسید چرا url را مستقیماً داخل بدنه useEffectEvent نخواندیم؟ وقتی url را به عنوان آرگومان به تابع onVisit(url) پاس میدهید، صراحتاً به ریآکت و لینتر اعلام میکنید که تغییر url یک رویداد مجزا و جدید است. این کار از حذف تصادفی url از آرایه وابستگی افکت جلوگیری میکند و برای کدهای ناهمگام (مثل setTimeout) تضمین میکند که دقیقاً همان آدرسی که باعث اجرای افکت شده، ثبت شود.
توابع ساخته شده با useEffectEvent ابزارهای بسیار محدودی هستند و باید فقط در چارچوب زیر استفاده شوند:
فقط و فقط باید از داخل یک useEffect صدا زده شوند.
هرگز نباید آنها را به کامپوننتهای دیگر یا هوکهای سفارشی پاس داد.
به این نمونه اشتباه و نحوه اصلاح آن نگاه کنید:
// ❌ ساختار اشتباه: پاس دادن Effect Event به یک هوک دیگر
function Timer() {
const [count, setCount] = useState(0);
const onTick = useEffectEvent(() => { setCount(count + 1); });
useTimer(onTick, 1000); // اشتباه
return <h1>{count}</h1>;
}
اصلاح اصولی: همیشه useEffectEvent را دقیقاً در همان جایی تعریف کنید که افکت مصرفکننده آن قرار دارد:
// ✅ ساختار درست و استاندارد
function Timer() {
const [count, setCount] = useState(0);
useTimer(() => { setCount(count + 1); }, 1000);
return <h1>{count}</h1>;
}
function useTimer(callback, delay) {
// تعریف دقیقاً در کنار افکتِ بومی خودِ هوک
const onTick = useEffectEvent(() => {
callback();
});
useEffect(() => {
const id = setInterval(() => { onTick(); }, delay);
return () => clearInterval(id);
}, [delay]); // نیازی به ذکر onTick در وابستگیها نیست
}
کدهای رویدادی (Events) در پاسخ به کارهای مستقیم کاربر اجرا میشوند، اما افکتها (Effects) برای همگامسازی دائم رابط کاربری با سیستم بیرونی هستند.
تمام مقادیر داخل بدنه کامپوننت واکنشپذیرند؛ تغییر آنها افکت را وادار به اجرای دوباره میکند.
به کمک هوک useEffectEvent میتوانید کدهای غیرواکنشپذیر را از دل افکت جدا کنید تا تغییرات آنها مزاحم فرآیند اصلی افکت نشود.
افکت ایونتها را به هیچ عنوان به کامپوننتها یا هوکهای دیگر پاس ندهید و آنها را منحصراً درون افکتهای محلی مصرف کنید.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://react.dev/learn