Cleanup در Effects: پاک‌سازی پیش از اجرای دوباره یا حذف کامپوننت

در درس قبلی دیدیم Effect چیست و Dependency Array چگونه زمان اجرای دوباره آن را کنترل می‌کند. اما بسیاری از Effectها، علاوه بر «شروع» یک کار (مانند اتصال به یک سرویس یا تنظیم یک تایمر)، به یک مرحله «پایان دادن» به همان کار هم نیاز دارند. این مرحله، Cleanup نام دارد.

مشکل: Effectی که چیزی را رها می‌کند

فرض کنید یک تایمر بسازیم که هر ثانیه مقدار یک شمارنده را افزایش می‌دهد:

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    setInterval(() => {
      setSeconds((s) => s + 1);
    }, 1000);
  }, []);

  return <p>{seconds}</p>;
}

این کد در نگاه اول کار می‌کند، اما یک مشکل جدی دارد: اگر کامپوننت Timer از صفحه حذف شود (مثلاً کاربر به صفحه دیگری برود)، تایمر همچنان در پس‌زمینه به کار خود ادامه می‌دهد و مدام تلاش می‌کند مقدار seconds را در کامپوننتی که دیگر وجود ندارد تغییر دهد. به این نوع مشکل، Memory Leak گفته می‌شود.

راه‌حل: بازگرداندن یک تابع Cleanup

برای رفع این مشکل، تابعی که به useEffect می‌دهیم، می‌تواند خودش یک تابع دیگر بازگرداند؛ این تابع بازگشتی، همان Cleanup است و React آن را در زمان مناسب فراخوانی می‌کند:

useEffect(() => {
  const id = setInterval(() => {
    setSeconds((s) => s + 1);
  }, 1000);

  return () => {
    clearInterval(id);
  };
}, []);

حالا وقتی کامپوننت از صفحه حذف می‌شود، React تابع Cleanup را فراخوانی می‌کند و تایمر با clearInterval به‌درستی متوقف می‌شود.

Cleanup فقط برای زمان حذف کامپوننت نیست

نکته مهمی که باید در نظر داشت این است که تابع Cleanup، فقط در لحظه حذف کامل کامپوننت اجرا نمی‌شود؛ بلکه هر بار پیش از اجرای دوباره خود Effect (به‌دلیل تغییر یکی از مقادیر Dependency Array) نیز، ابتدا Cleanup مربوط به اجرای قبلی فراخوانی می‌شود، و سپس نسخه جدید Effect اجرا می‌شود. این الگو تضمین می‌کند که همیشه، پیش از شروع یک اتصال جدید، اتصال قبلی به‌درستی بسته شده باشد.

useEffect(() => {
  console.log("اتصال به کاربر", userId);

  return () => {
    console.log("قطع اتصال از کاربر", userId);
  };
}, [userId]);

اگر userId تغییر کند، ترتیب اجرا به این شکل است: ابتدا Cleanup مربوط به userId قبلی («قطع اتصال از کاربر ۱»)، سپس بدنه اصلی Effect برای userId جدید («اتصال به کاربر ۲»).

چه زمانی به Cleanup نیاز داریم

Cleanup در هر موقعیتی لازم است که Effect چیزی را «شروع» می‌کند که باید در جایی «پایان» یابد. چند نمونه رایج:

  • تایمرها: setInterval باید با clearInterval متوقف شود.
  • Event Listenerها: هر addEventListener باید با removeEventListener حذف شود.
  • اشتراک در یک منبع داده خارجی: باید هنگام حذف کامپوننت، لغو اشتراک انجام شود.

اگر Effect فقط یک عملیات ساده و بدون وضعیت پایدار انجام می‌دهد (مانند تنظیم document.title)، نیازی به Cleanup ندارد.

مثال قابل اجرا

تایمر با Cleanup صحیح
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

تابعی که یک Effect بازمی‌گرداند، به‌عنوان Cleanup شناخته می‌شود و برای متوقف کردن یا لغو کاری که خود Effect شروع کرده، استفاده می‌شود. این Cleanup هم پیش از حذف کامل کامپوننت، و هم پیش از هر اجرای دوباره Effect به‌دلیل تغییر Dependencyها، فراخوانی می‌شود. در درس بعدی به موضوع Synchronizing with External Systems می‌پردازیم که دید عمیق‌تری به همین موضوع می‌دهد.