برخی از کامپوننتها نیاز دارند تا با محیطهای خارج از ریآکت همگام یا اصطلاحاً Synchronize شوند؛ برای نمونه، کنترل یک پلاگین ویدیوپلیر قدیمی مرورگر بر اساس استیت ریآکت، برقراری اتصال با یک سرور گفتگوی زنده (Socket)، یا ارسال لاگهای تحلیلی (Analytics) به محض ورود کاربر به صفحه.
هوک useEffect به شما اجازه میدهد کدهایی را دقیقاً پس از اتمام رندر و آپدیت شدن صفحه اجرا کنید تا بتوانید کامپوننت خود را با هر سیستم خارج از ریآکتی هماهنگ نگه دارید.
افکتها (Effects) دقیقاً چه هستند و چه تفاوتی با رویدادها (Events) دارند؟
چطور یک Effect در کامپوننت تعریف کنیم؟
چطور از اجرای غیرضروری و تکراری یک Effect جلوگیری کنیم؟
چرا افکتها در محیط Development دو بار اجرا میشوند و چطور کدهایمان را اصلاح کنیم؟
پیش از بررسی کدهای useEffect، باید مرز میان دو بخش از منطق درون کامپوننتهای ریآکت را به خوبی درک کنیم:
کدهای رندرینگ (Rendering Code): این بخش در بالاترین سطح بدنه کامپوننت شما قرار دارد. جایی که پروپها و استیتها را تحویل میگیرید، آنها را پردازش کرده و در نهایت خروجی JSX را برمیگردانید. کدهای این بخش باید کاملاً Pure (ناب) باشند؛ یعنی مانند یک فرمول ریاضی، صرفاً خروجی را محاسبه کنند و هیچ کار جانبی دیگری (مانند تغییر متغیرهای بیرونی) انجام ندهند.
مدیریتکنندههای رویداد (Event Handlers): اینها توابع داخلی کامپوننت شما هستند که کار عملی انجام میدهند (نه صرفاً محاسبه خروجی)؛ مانند ثبت فرم، تغییر فیلد متنی یا ارسال درخواست HTTP POST. این توابع حاوی Side Effects (عوارض جانبی) هستند و مستقیماً توسط یک رفتار مشخص از سوی کاربر (مثل کلیک روی یک دکمه یا تایپ کردن) لانچ میشوند.
اما گاهی این دو حالت کافی نیستند! فرض کنید به محض باز شدن صفحه چت، کامپوننت شما باید بدون معطلی به سرور متصل شود. برقراری اتصال سرور یک محاسبه ریاضی ناب نیست، پس جایش در بدنه رندر نیست. از طرفی، هیچ دکمه یا رویداد کلیکی هم وجود ندارد که کاربر آن را بفشارد تا اتصال برقرار شود؛ این اتصال صرفاً به خاطر ظاهر شدن خودِ کامپوننت روی صفحه رخ میدهد.
💡 قاعده کلی: رویدادها (Events) ناشی از یک اقدام مستقیم کاربر هستند، در حالی که افکتها (Effects) ناشی از فرآیند رندر شدن خودِ کامپوننت میباشند. افکتها در آخرین مرحله و پس از اینکه کاربر تغییرات جدید را روی صفحه دید، اجرا میشوند.
برای پیادهسازی یک افکت اصولی، ۳ گام زیر را دنبال میکنیم:
ابتدا هوک useEffect را ایمپورت کرده و آن را در بالاترین سطح کامپوننت صدا میزنیم:
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// کدهای داخل این بخش پس از "هر بار رندر شدن" کامپوننت اجرا میشوند
});
return <div />;
}
بیایید یک مثال واقعی بزنیم. فرض کنید میخواهید یک ویدیوپلیر بومی مرورگر (<video>) را با پروپ isPlaying در ریآکت کنترل کنید. تگ ویدیو در HTML پروپی به نام isPlaying ندارد و تنها راه پخش یا توقف آن، صدا زدن متدهای نیتیو .play() و .pause() روی خودِ المان DOM است.
اگر این متدها را مستقیماً در بدنه رندر صدا بزنید، برنامه کرش خواهد کرد؛ زیرا در اولین رندر، هنوز المان DOM ایجاد نشده است! راهکار درست، انتقال این کار به داخل useEffect است:
import { useEffect, useRef } from 'react';
function VideoPlayer({ src, isPlaying }) {
const videoRef = useRef(null);
useEffect(() => {
// این کد پس از رندر و ساخته شدن DOM اجرا میشود، پس کاملاً امن است
if (isPlaying) {
videoRef.current.play();
} else {
videoRef.current.pause();
}
});
return <video ref={videoRef} src={src} loop playsInline />;
}
به صورت پیشفرض، کدهای درون useEffect بعد از هر رندر پیاپی اجرا میشوند. فرض کنید در کنار ویدیوپلیر، یک فیلد متنی ساده (Input) هم دارید. با هر بار تایپ کردن کاربر درون اینپوت، استیت کامپوننت تغییر کرده، کامپوننت دوباره رندر میشود و در نتیجه افکت ویدیوپلیر مجدداً اجرا میگردد! این موضوع اصلاً بهینه نیست.
برای حل این چالش، آرایهای از وابستگیها (Dependency Array) را به عنوان آرگومان دوم به useEffect پاس میدهیم:
useEffect(() => {
if (isPlaying) {
videoRef.current.play();
} else {
videoRef.current.pause();
}
}, [isPlaying]); // افکت تنها و تنها زمانی دوباره اجرا میشود که مقدار isPlaying تغییر کرده باشد
بدون پاس دادن آرایه: افکت بعد از هر بار رندر شدن کامپوننت اجرا میشود.
پاس دادن آرایه خالی []: افکت فقط یک بار و در لحظه متولد شدن (Mount) کامپوننت روی صفحه اجرا میشود.
پاس دادن آرایه همراه با متغیر [a, b]: افکت در لحظه Mount و همچنین در رندرهای بعدی، فقط در صورت تغییر هر کدام از مقادیر a یا b اجرا میشود.
یک سناریوی دیگر را در نظر بگیرید: کامپوننت اتاق گفتگو (ChatRoom) به محض باز شدن باید به سرور متصل شود. کد آن را با آرایه وابستگی خالی مینویسیم تا فقط یکبار زمان باز شدن اجرا شود:
useEffect(() => {
const connection = createConnection();
connection.connect();
}, []);
اما اگر کاربر از این صفحه به صفحه «تنظیمات» برود و کامپوننت چت از روی صفحه حذف (Unmount) شود چه؟ اتصال قبلی همچنان در پسزمینه مرورگر باز و زنده میماند! اگر کاربر مدام بین صفحات جابجا شود، صدها اتصال فعال و رها شده به وجود میآید.
برای جلوگیری از این باگ، باید یک تابع پاکسازی (Cleanup Function) از درون افکت ریترن (return) کنیم:
useEffect(() => {
const connection = createConnection();
connection.connect();
// تابع پاکسازی
return () => {
connection.disconnect(); // قطع اتصال در زمان ترک صفحه یا رندر بعدی
};
}, []);
اگر کدهای بالا را در محیط دولوپمنت تست کنید و کنسول مرورگر را باز کنید، با پدیده عجیبی مواجه میشوید؛ پیام اتصال سرور را دو بار پشت سر هم میبینید:
✅ Connecting...
❌ Disconnected.
✅ Connecting...
آیا این یک باگ است؟ خیر! ریآکت در حالت توسعه و به دلیل فعال بودن Strict Mode، به عمد یکبار کامپوننت شما را متولد میکند، فوراً آن را حذف کرده و مجدداً متولد میسازد (Mount -> Unmount -> Mount).
ریآکت این رفتار سختگیرانه را انجام میدهد تا به شما ثابت کند آیا تابع پاکسازی (Cleanup) خود را به درستی پیاده کردهاید یا خیر. اگر قطع اتصال درست کار کند، این جابجایی سریع هیچ باگ یا نشت حافظهای در مرورگر ایجاد نخواهد کرد. در محیط تولید (Production)، این رفتار حذف شده و افکت دقیقاً یک بار اجرا میشود.
افکتها برخلاف رویدادها، به خاطر رندر شدن خودِ کامپوننت اجرا میشوند نه رفتار مستقیم کاربر.
با استفاده از هوک useEffect میتوانید با سیستمهای خارج از ریآکت (کدهای نیتیو مرورگر، درخواستهای شبکه و...) تعامل داشته باشید.
برای جلوگیری از افت فریم و رندرهای تکراری، همواره متغیرهای مورد استفاده درون افکت را در آرایه وابستگیها ذکر کنید.
برای کارهایی که نیاز به لغو یا متوقفسازی دارند (مثل اینتروالها یا اتصالات سوکت)، حتماً یک تابع بازگشتی جهت Cleanup بنویسید.
اجرای دوبارۀ افکتها در محیط لکال، رفتار عمدی ریآکت برای کشف نشت حافظه و باگهای پنهان کدهای شماست.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://react.dev/learn