هر کامپوننت ریآکت چرخه حیات مشخصی را طی میکند:
متولد شدن (Mount): زمانی که کامپوننت برای اولین بار به صفحه اضافه میشود.
بهروزرسانی (Update): زمانی که کامپوننت پروپ یا استیت جدیدی دریافت میکند (معمولاً در پاسخ به یک تعامل).
مرگ (Unmount): زمانی که کامپوننت از روی صفحه حذف میشود.
فرمول بالا ابزار ذهنی فوقالعادهای برای درک کامپوننتها است، اما برای درک افکتها اصلاً مناسب نیست! به جای آن، سعی کنید به هر Effect به طور کاملاً مستقل از چرخه حیات کامپوننت نگاه کنید. یک افکت صرفاً توصیف میکند که چطور سیستم خارجی با پروپها و استیتهای فعلی همگام (Synchronize) شود. با تغییر دادهها، این همگامسازی ممکن است بارها شروع و متوقف شود.
چرا همگامسازی افکتها ممکن است فراتر از عمر کامپوننت، بارها تکرار شود؟
ریآکت چگونه یک Effect را مجدداً همگامسازی (Re-synchronize) میکند؟
چطور باید از زاویه دید یک Effect به برنامهنویسی نگاه کنیم؟
چرا هر فرآیند همگامسازی باید یک useEffect کاملاً مجزا داشته باشد؟
تصور کنید کامپوننت اتاق گفتگو (ChatRoom) پروپی به نام roomId دریافت میکند که کاربر آن را از یک منوی کشویی (Dropdown) انتخاب میکند. در ابتدا کاربر اتاق "general" را انتخاب میکند. ریآکت کامپوننت را رندر کرده و افکت را برای اتصال به این اتاق اجرا میکند:
// بدنه افکت: آغاز همگامسازی با اتاق general
const connection = createConnection(serverUrl, 'general');
connection.connect();
کمی بعد، کاربر اتاق گفتگو را به "travel" تغییر میدهد. ابتدا ریآکت ظاهر صفحه را بهروز میکند. اما مشکلی وجود دارد: ظاهر صفحه اتاق "travel" را نشان میدهد، اما افکت رندر قبلی هنوز به اتاق "general" متصل است! در این لحظه دیتای سیستم خارجی با رابط کاربری همخوانی ندارد.
در این نقطه، ریآکت باید دو کار انجام دهد:
توقف همگامسازی قدیمی: قطع اتصال از اتاق "general"
آغاز همگامسازی جدید: برقراری اتصال با اتاق "travel"
خوشبختانه شما پیش از این، هر دو مسیر را به ریآکت یاد دادهاید! بدنه افکت روش شروع و تابع کلینآپ روش توقف را مشخص میکند.
وقتی پروپ roomId از "general" به "travel" تغییر میکند، ریآکت مراحل زیر را طی میکند:
اجرای کلینآپ رندر قبلی: ریآکت تابع پاکسازی مربوط به اتاق "general" را صدا میزند تا اتصال قبلی کاملاً قطع شود:
// گره خورده به رندر قبلی با roomId = "general"
return () => { connection.disconnect(); };
اجرای بدنه افکت جدید: حالا ریآکت کدهای بدنه اصلی useEffect را با مقدار جدید یعنی "travel" اجرا میکند:
// گره خورده به رندر فعلی با roomId = "travel"
const connection = createConnection(serverUrl, 'travel');
connection.connect();
اگر کاربر صفحه را کاملاً ترک کند و کامپوننت حذف (Unmount) شود، ریآکت برای آخرین بار تابع کلینآپ را صدا میزند و اتصال اتاق فعلی (مثلاً "travel") را برای همیشه قطع میکند.
بیایید تفاوت دو تفکر ذهنی را بررسی کنیم:
«افکت من یک کالبک یا رویداد چرخه حیات است که یک بار بعد از رندر اول اجرا میشود، بعد موقع آپدیت شدن فلان کار را میکند و قبل از حذف شدن هم کدهای دیگر را اجرا میکند.»
این نگاه خیلی زود کدهای شما را پیچیده و پر از باگ میکند.
به جای تعقیب فرآیند رندر، روی یک چرخه واحدِ شروع/توقف (Start/Stop) تمرکز کنید:
useEffect(() => {
// ۱. من به اتاقی کهroomId مشخص کرده متصل میشوم...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
// ۲. ...تا زمانی که اتصال را قطع کنم.
connection.disconnect();
};
}, [roomId]);
برای شما نباید مهم باشد که کامپوننت در حال متولد شدن است، آپدیت میشود یا میمیرد. شما فقط یاد میدهید که چطور سیستم همگامسازی شروع به کار کند و چطور متوقف شود. اگر این دو بخش را درست بنویسید، افکت شما در برابر صدها بار قطع و وصل شدن ناگهانی هم کاملاً مقاوم (Resilient) خواهد بود.
هرگز منطقهای بیربط را صرفاً به این دلیل که قرار است در یک زمان اجرا شوند، داخل یک useEffect مشترک نریزید.
به این نمونه نامناسب نگاه کنید:
// ❌ تداخل دو فرآیند بیربط در یک افکت
useEffect(() => {
logVisit(roomId); // فرآیند ۱: ثبت آمار بازدید
const connection = createConnection(serverUrl, roomId); // فرآیند ۲: اتصال شبکه
connection.connect();
return () => connection.disconnect();
}, [roomId]);
مشکل این کد چیست؟ فرض کنید در آینده متغیر دیگری (مثلاً توکن اتصال سرور یا یک تم ظاهری) به وابستگیهای این افکت اضافه شود که نیاز داشته باشد اتصال سرور را مجدداً برقرار کند. با هر بار اجرای دوباره افکت، تابع logVisit نیز مجدداً صدا زده میشود و آمار بازدیدهای تکراری و دروغین به سرور آمار شما ارسال میگردد!
ثبت آمار بازدید و اتصال به سوکت چت، دو فرآیند کاملاً مستقل هستند. تفکیک اصولی آنها به این شکل است:
// ✅ تفکیک هوشمندانه به دو فرآیند مجزا و مستقل
function ChatRoom({ roomId }) {
// افکت اول: فقط مسئول همگامسازی آمار بازدید
useEffect(() => {
logVisit(roomId);
}, [roomId]);
// افکت دوم: فقط مسئول همگامسازی اتصال شبکه چت
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
// ...
}
یک نشانه برای تشخیص درست این است: اگر حذف کردن یکی از افکتها، منطق افکت دیگر را خراب نکند، یعنی شما کار تفکیک را به بهترین شکل ممکن انجام دادهاید.
افکتها به جای چرخه حیات کامپوننت (Mount/Update/Unmount)، بر اساس چرخه حیات خود یعنی «شروع و پایان همگامسازی» زندگی میکنند.
تغییر در آرایه وابستگیها باعث میشود ریآکت ابتدا کلینآپ رندر قبلی را اجرا کرده و سپس افکت را با مقادیر جدید بازخوانی کند.
همیشه به صورت مجزا به یک فاز متصل شدن و قطع شدن نگاه کنید تا کدهایی پایدار داشته باشید.
هر افکت باید عهدهدار یک وظیفه یا یک فرآیند همگامسازی واحد باشد؛ کارهای موازی و بیربط را در افکتهای مجزا بنویسید.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://react.dev/learn