React می‌خواهد کامپوننت‌های شما پیش‌بینی‌پذیر باشند. اگر همان state و props را به آن بدهید، باید هر بار همان UI را ترسیم کند. اما اکثر اپلیکیشن‌های واقعی نمی‌توانند درون آن حباب باقی بمانند؛ آن‌ها نیاز دارند با دنیای بیرون ارتباط برقرار کنند. یک داشبورد به اعداد تازه از یک سرور نیاز دارد. یک ویجت چت باید منتظر پیام‌ها باشد. یک تایمر باید تیک بزند. این عملیات‌ها side effect هستند و خارج از چرخه رندر React قرار می‌گیرند. هوک useEffect جایی است که آن کارهای نامنظم و غیرقابل پیش‌بینی را در آن قرار می‌دهید تا خودِ کامپوننت شما خالص و قابل اعتماد باقی بماند.

Side Effects: چه چیزهایی باید داخل useEffect باشند

یک side effect هر چیزی است که فراتر از بازگرداندن JSX، با دنیای بیرون درگیر شود. فاز رندر React باید pure باشد. وقتی شروع به fetch کردن داده‌ها، نوشتن در متغیرهای global یا متصل کردن listenerها به DOM می‌کنید، از قلمرو pure خارج شده‌اید.

مثال‌های رایج عبارتند از:

  • دریافت داده از یک API
  • تنظیم تایمرها یا intervalها
  • اضافه کردن event listenerها به window یا document
  • به‌روزرسانی عنوان تب مرورگر
  • اتصال به WebSockets

این وظایف یک ویژگی مشترک دارند: آن‌ها نباید داخل دستور return کامپوننت یا در طول منطق اصلی رندر باشند. تلاش برای فراخوانی یک browser API مانند setInterval مستقیماً داخل بدنه رندر، باعث می‌شود در هر بار رندر اجرا شود که منجر به ایجاد تایمرهای تکراری و رفتارهای گیج‌کننده می‌شود. useEffect دقیقاً برای ایزوله کردن این کارها و اجرای آن‌ها در لحظه مناسب وجود دارد.

آرایه وابستگی (Dependency Array) چگونه زمان‌بندی را کنترل می‌کند

آرگومان دوم برای useEffect همان dependency array است و این بزرگترین منبع سردرگمی برای توسعه‌دهندگانی است که از class components به سمت کامپوننت‌های تابعی می‌آیند. آن را به عنوان مجموعه‌ای از متغیرها در نظر بگیرید که React آن‌ها را زیر نظر می‌گیرد تا تصمیم بگیرد آیا بعد از رندر فعلی، effect شما را اجرا کند یا از آن صرف‌نظر کند.

سه الگوی وجود دارد که شما مکرراً از آن‌ها استفاده خواهید کرد.

اصلاً آرایه وابستگی وجود ندارد. اگر آرایه را کاملاً حذف کنید، React فرض می‌کند که می‌خواهید effect بعد از هر رندر (از جمله اولین رندر) اجرا شود. این حالت به ندرت چیزی است که نیاز دارید. اگر effect شما یک درخواست شبکه یا یک عملیات سنگین DOM انجام می‌دهد، اجرای آن با هر بار تایپ کردن یا تغییر state، عملکرد (performance) را به شدت کاهش می‌دهد. تنها زمانی از این الگو استفاده کنید که واقعاً نیاز دارید چیزی را دوباره اجرا کنید، اما نمی‌توانید مشخص کنید کدام prop یا state تغییر کرده است.

یک آرایه خالی []. این به React می‌گوید که effect را فقط یک بار، بلافاصله پس از mount شدن کامپوننت و آماده شدن DOM اجرا کند. این جای مناسب برای fetch کردن داده‌های اولیه است. به عنوان مثال، اگر کامپوننت شما داده‌های پروفایل کاربر را بارگذاری می‌کند، می‌خواهید آن درخواست دقیقاً یک بار زمانی که صفحه پروفایل ظاهر می‌شود اجرا شود، نه هر بار که کاربر با فرمی در پایین صفحه تعامل دارد.

آرایه‌ای با متغیرهای مشخص [count]. این یک ابزار دقیق است. React مقادیر فعلی این وابستگی‌ها را با مقادیر آن‌ها در آخرین رندر مقایسه می‌کند. اگر هر یک از آن‌ها تغییر کرده باشد، effect اجرا می‌شود. اگر هیچ چیز در لیست تغییر نکرده باشد، React کاملاً از اجرای effect صرف‌نظر می‌کند.

اگر در حال همگام‌سازی عنوان تب مرورگر با یک متغیر state هستید، باید آن متغیر را در dependency array قرار دهید. در این صورت React عنوان را فقط زمانی که آن مقدار تغییر کند به‌روز می‌کند. اگر آن را فراموش کنید، عنوان قدیمی (stale) باقی می‌ماند. اگر متغیرهای state بی‌ربط را در آن قرار دهید، چرخه پردازنده را برای به‌روزرسانی عنوانی که تغییری در آن اهمیت ندارد، هدر می‌دهید.

پاکسازی (Cleanup) اختیاری نیست

برخی از effectها ردپایی از خود به جای می‌گذارند. یک تایمر به شمارش ادامه می‌دهد. یک event listener همچنان اجرا می‌شود. یک WebSocket باز می‌ماند. وقتی کامپوننت شما unmount می‌شود، یا حتی وقتی یک effect به دلیل تغییر وابستگی‌هایش دوباره اجرا می‌شود، React به طور خودکار باقی‌مانده‌های effect قبلی را پاکسازی نمی‌کند. این وظیفه شماست.

شما با بازگرداندن (return) یک تابع از داخل useEffect ، یک تابع پاکسازی (cleanup function) ایجاد می‌کنید. React این تابع پاکسازی را قبل از اعمال effect بعدی، و یک بار دیگر زمانی که کامپوننت از صفحه خارج می‌شود، فراخوانی می‌کند.

شما باید از cleanup برای موارد زیر استفاده کنید:

  • پاک کردن intervalها یا timeoutها با استفاده از clearInterval یا clearTimeout
  • حذف event listenerهایی که به window ، document یا گره‌های (nodes) خارجی اضافه شده‌اند
  • لغو اشتراک (unsubscribing) از جریان‌های داده یا سرویس‌ها

اگر به این موضوع بی‌توجهی کنید، با memory leak مواجه خواهید شد. یک کامپوننت mount می‌شود، یک scroll listener به آن متصل می‌شود، سپس unmount می‌شود، اما listener همچنان باقی می‌ماند. مرورگر callback و گره‌های DOM که به آن‌ها ارجاع داده شده را نگه می‌دارد. با گذشت زمان، به ویژه در اپلیکیشن‌های تک‌صفحه‌ای (SPA) با ناوبری زیاد، این «ارواح» انباشته شده و تب را کند می‌کنند. راه حل معمولاً فقط چند خط است: تابعی را بازگردانید که آنچه را اضافه کرده بودید، حذف کند.

اشتباهات رایجی که به محیط Production می‌روند

حتی توسعه‌دهندگان باتجربه هم وقتی گزینه‌ی ساده‌تری وجود دارد، به سراغ useEffect می‌روند. در اینجا سه الگو آورده شده است که باید در طول بازبینی کد (code review) به عنوان یک هشدار (red flag) در نظر گرفته شوند.

حلقه‌های بی‌نهایت. هرگز یک متغیر وضعیت (state variable) را در داخل useEffect به‌روزرسانی نکنید، اگر همان متغیر در آرایه‌ی وابستگی (dependency array) شما قرار دارد، مگر اینکه یک شرط کنترلی (gate condition) داشته باشید که چرخه را بشکند. اگر count را بخوانید، آن را افزایش دهید و count را به عنوان یک وابستگی لیست کنید، React تغییر را مشاهده می‌کند، دوباره رندر (re-render) می‌کند، اثر (effect) را دوباره اجرا می‌کند، دوباره آن را افزایش می‌دهد و مرورگر را قفل می‌کند.

اثرات (effects) غیرضروری. از useEffect برای محاسبه‌ی یک مقدار از روی props یا state موجود استفاده نکنید. اگر می‌توانید آن را مستقیماً در طول رندر استخراج کنید، فقط همین کار را انجام دهید. مقادیر مشتق‌شده (derived values) باید در بدنه کامپوننت یا در یک محاسبه‌ی ذخیره‌شده (memoized) با useMemo قرار بگیرند. انتقال آن‌ها به یک effect، منطق شما را بدون هیچ فایده‌ای بین مراحل رندر و effect تقسیم می‌کند و دنبال کردن کد را دشوارتر می‌سازد.

ابزار اشتباه برای اقدامات کاربر. `use