همانطور که میدانید، مدیریتکنندههای رویداد (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 دسترسی داشته باشد.
۳. راهکار اصولی: تعریف یک Effect Event
برای حل این پارادوکس، ریآکت هوک ویژهای به نام 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) تضمین میکند که دقیقاً همان آدرسی که باعث اجرای افکت شده، ثبت شود.
۵. محدودیتهای سختگیرانه Effect Events
توابع ساخته شده با 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 در وابستگیها نیست
}
خلاصه نکات کلیدی (Recap)
-
کدهای رویدادی (Events) در پاسخ به کارهای مستقیم کاربر اجرا میشوند، اما افکتها (Effects) برای همگامسازی دائم رابط کاربری با سیستم بیرونی هستند.
-
تمام مقادیر داخل بدنه کامپوننت واکنشپذیرند؛ تغییر آنها افکت را وادار به اجرای دوباره میکند.
-
به کمک هوک
useEffectEventمیتوانید کدهای غیرواکنشپذیر را از دل افکت جدا کنید تا تغییرات آنها مزاحم فرآیند اصلی افکت نشود. -
افکت ایونتها را به هیچ عنوان به کامپوننتها یا هوکهای دیگر پاس ندهید و آنها را منحصراً درون افکتهای محلی مصرف کنید.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: منبع مستندات