همان‌طور که از نام آن پیداست، تابع cleanup در هوک useEffect وظیفه پاک‌سازی اثرات جانبی ناخواسته را بر عهده دارد و از بروز رفتارهای غیرمنتظره در اپلیکیشن جلوگیری می‌کند. این تابع به ما اجازه می‌دهد پیش از آن‌که کامپوننت از صفحه خارج شود (unmount)، کدهای مرتبط را پاک‌سازی و مرتب کنیم.

هر بار که کامپوننت رندر می‌شود و useEffect مجدداً اجرا می‌گردد، ابتدا تابع cleanup اجرا می‌شود تا اثرات باقی‌مانده از اجرای قبلی حذف شود، سپس useEffect دوباره فراخوانی می‌گردد. زمان اجرای useEffect به آرایه وابستگی‌ها بستگی دارد:

در طراحی هوک useEffect، امکان بازگرداندن یک تابع از درون آن فراهم شده است؛ این تابع همان تابع cleanup است. وظیفه اصلی این تابع، جلوگیری از memory leak است؛ حالتی که اپلیکیشن تلاش می‌کند state بخشی از حافظه‌ای را تغییر دهد که دیگر وجود ندارد. علاوه بر آن، تابع cleanup برای حذف رفتارهای جانبی ناخواسته نیز کاربرد دارد.

نکته مهمی که باید رعایت شود این است که نباید درون تابع state ،cleanup را تغییر دهیم؛ چرا که این کار ممکن است منجر به رفتارهای پیش‌بینی‌نشده در اپلیکیشن شود.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
useEffect(() => {
effect;
return () => {
cleanup;
};
}, [input]);
useEffect(() => { effect; return () => { cleanup; }; }, [input]);
useEffect(() => {
  effect;
  return () => {
    cleanup;
  };
}, [input]);

چرا تابع cleanup هوک useEffect کاربردی است؟

تابع cleanup در هوک useEffect به توسعه‌دهندگان کمک می‌کند اثراتی را که ممکن است منجر به رفتارهای ناخواسته شوند، از بین ببرند و در نتیجه عملکرد اپلیکیشن را بهینه نمایند.

اما نکته قابل توجه این است که این تابع فقط هنگام unmount شدن کامپوننت اجرا نمی‌شود؛ بلکه پیش از اجرای اثر جدید بعدی نیز فعال می‌گردد.

در واقع، بعد از اجرای هر effect، اجرای بعدی بر اساس آرایه وابستگی‌ها زمان‌بندی می‌شود:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// The `dependency` in the code below is an array
useEffect(callback, dependency)
// The `dependency` in the code below is an array useEffect(callback, dependency)
// The `dependency` in the code below is an array
useEffect(callback, dependency)

بنابراین، هرگاه effect ما به یک prop وابسته باشد، یا ما چیزی راه‌اندازی کنیم که حالت ماندگار داشته باشد، دلایل قانع‌کننده‌ای برای اجرای تابع cleanup خواهیم داشت.

یک سناریو را بررسی می‌کنیم: فرض کنید می‌خواهیم اطلاعات یک کاربر خاص را با استفاده از

id
id او از سرور دریافت کنیم. اما پیش از آن که این درخواست کامل شود، نظرمان عوض می‌شود و تصمیم می‌گیریم اطلاعات کاربر دیگری را دریافت نماییم.

در این شرایط، هر دو درخواست fetch به کار خود ادامه می‌دهند، حتی اگر کامپوننت از بین رفته باشد یا وابستگی‌ها تغییر کرده باشند. این می‌تواند به رفتارهای غیرمنتظره یا خطاهایی منجر شود، مثل نمایش اطلاعات قدیمی یا تلاش برای به‌روزرسانی کامپوننتی که دیگر در DOM وجود ندارد. بنابراین باید با استفاده از تابع cleanup، درخواست fetch قبلی را متوقف کنیم.

چه زمانی باید از تابع cleanup هوک useEffect استفاده کنیم؟

فرض کنید یک کامپوننت React داریم که داده‌هایی را از سرور دریافت کرده و نمایش می‌دهد. اگر کامپوننت ما قبل از اتمام اجرای Promise از بین برود، هوک useEffect سعی می‌کند state را در کامپوننتی که دیگر وجود ندارد به‌روزرسانی کند. در این حالت، React هشدار زیر را نمایش می‌دهد:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in Post (at App.js:13)
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. in Post (at App.js:13)
Warning: Can't perform a React state update on an unmounted component. 
This is a no-op, but it indicates a memory leak in your application. 
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
    in Post (at App.js:13)

این هشدار در نسخه‌های ۱۷ و پایین‌تر React نمایش داده می‌شود و در نسخه React 18 حذف شده است. علت حذف آن این بود که React در واقع راه دقیقی برای تشخیص memory leak ندارد. در نسخه‌های قبل، هرگونه به‌روزرسانی state پس از unmount شدن کامپوننت، به عنوان احتمال memory leak علامت‌گذاری می‌شد، در حالی که اغلب این موارد واقعاً memory leak نبودند. در نتیجه، توسعه‌دهندگان زمان زیادی را صرف نوشتن راه‌حل‌هایی برای حذف این هشدارهای کاذب می‌کردند.

اگرچه در نسخه ۱۸ به بعد این هشدار دیگر نمایش داده نمی‌شود، اما همان‌طور که در ادامه خواهیم دید، همچنان باید از تابع cleanup استفاده کنیم تا subscriptionها و سایر side effectهایی که ممکن است باعث memory leak شوند، لغو گردند. همچنین، در مواقع لازم باید درخواست‌های fetch را نیز لغو کنیم تا تجربه کاربری بهتری فراهم شود. در ادامه نیز به بررسی چند راه‌کار برای حذف هشدار بالا در نسخه‌های پایین‌تر React خواهیم پرداخت.

طبق مستندات رسمی React:

«تابع cleanup در هوک useEffect نه‌تنها در زمان unmount شدن اجرا می‌شود، بلکه پیش از هر بار رندر مجدد که در آن وابستگی‌ها تغییر کرده باشند نیز اجرا خواهد شد. همچنین، در حالت development، ری‌اکت پس از mount شدن کامپوننت، فرایند setup و cleanup را یک بار دیگر نیز اجرا می‌کند.»

نکته‌ای پیش از ادامه بحث:

با ارسال یک آرایه خالی به‌عنوان لیست وابستگی‌ها، می‌توانیم تعیین کنیم که useEffect فقط یک‌بار اجرا شود. وقتی آرایه وابستگی خالی باشد، یعنی effect به هیچ‌یک از مقادیر state یا prop وابسته نیست. در نتیجه فقط پس از رندر اولیه اجرا خواهد شد و در رندرهای بعدی اجرا نمی‌شود؛ مگر این‌که کامپوننت unmount و دوباره mount شود:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
useEffect(() => {
// Effect implementation
}, []); // Empty dependency array indicates the effect should only run once
useEffect(() => { // Effect implementation }, []); // Empty dependency array indicates the effect should only run once
useEffect(() => {
// Effect implementation
}, []); // Empty dependency array indicates the effect should only run once

اکنون که متوجه شدیم چگونه می‌توانیم هوک useEffect را فقط یک‌بار اجرا کنیم، به بحث تابع cleanup برمی‌گردیم.

تابع cleanup معمولاً برای لغو subscriptionها و درخواست‌های async فعال استفاده می‌شود. در ادامه یک مثال را بررسی می‌کنیم تا ببینیم چگونه می‌توانیم این لغوها را پیاده‌سازی نماییم.

پاک‌سازی یک Subscription

برای شروع پاک‌سازی یک Subscription، ابتدا باید آن را unsubscribe کنیم . این کار مانع از بروز memory leak در اپلیکیشن می‌شود و به بهینه‌سازی عملکرد کمک می‌کند.

برای لغو Subscription پیش از unmount شدن کامپوننت، می‌توانیم متغیری به نام

isApiSubscribed
isApiSubscribed تعریف کنیم و در ابتدا مقدار آن را برابر
true
true قرار دهیم. سپس هنگام unmount شدن، آن را به
false
false تغییر دهیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
useEffect(() => {
// set our variable to true
let isApiSubscribed = true;
axios.get(API).then((response) => {
if (isApiSubscribed) {
// handle success
}
});
return () => {
// cancel the subscription
isApiSubscribed = false;
};
}, []);
useEffect(() => { // set our variable to true let isApiSubscribed = true; axios.get(API).then((response) => { if (isApiSubscribed) { // handle success } }); return () => { // cancel the subscription isApiSubscribed = false; }; }, []);
useEffect(() => {
  // set our variable to true
  let isApiSubscribed = true;
  axios.get(API).then((response) => {
    if (isApiSubscribed) {
      // handle success
    }
  });
  return () => {
    // cancel the subscription
    isApiSubscribed = false;
  };
}, []);

لغو یک درخواست fetch

برای لغو یک درخواست fetch، می‌توانیم از دو روش استفاده کنیم: یا از API مربوط به

AbortController
AbortController بهره بگیریم یا از Axios cancel token.

برای استفاده از

AbortController
AbortController، باید یک کنترلر با constructor
AbortController()
AbortController() ایجاد کنیم. سپس هنگام شروع درخواست fetch،
AbortSignal
AbortSignal را به عنوان گزینه درون آبجکت
option
option درخواست ارسال می‌نماییم.

این کار باعث می‌شود که کنترلر و سیگنال با درخواست fetch مرتبط شوند و بتوانیم در هر زمان دلخواه، با فراخوانی متد

AbortController.abort()
AbortController.abort() آن را لغو کنیم.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
fetch(API, {
signal: signal,
})
.then((response) => response.json())
.then((response) => {
// handle success
});
return () => {
// cancel the request before component unmounts
controller.abort();
};
}, []);
useEffect(() => { const controller = new AbortController(); const signal = controller.signal; fetch(API, { signal: signal, }) .then((response) => response.json()) .then((response) => { // handle success }); return () => { // cancel the request before component unmounts controller.abort(); }; }, []);
useEffect(() => {
  const controller = new AbortController();
  const signal = controller.signal;
  fetch(API, {
    signal: signal,
  })
    .then((response) => response.json())
    .then((response) => {
      // handle success
    });
  return () => {
    // cancel the request before component unmounts
    controller.abort();
  };
}, []);

با این رویکرد، می‌توانیم مدیریت خطا را نیز بهینه نماییم. کافی است در بلاک

catch
catch بررسی کنیم که آیا خطا ناشی از عملیات abort بوده یا خیر. اگر چنین باشد، دیگر نیازی به به‌روزرسانی state نخواهیم داشت. این کار از به‌روزرسانی‌های غیرضروری در هنگام خروج از کامپوننت جلوگیری می‌کند و مدیریت lifecycle را بهبود می‌بخشد.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
fetch(API, {
signal: signal
})
.then((response) => response.json())
.then((response) => {
// handle success
console.log(response);
})
.catch((err) => {
if (err.name === 'AbortError') {
console.log('successfully aborted');
} else {
// handle error
}
});
return () => {
// cancel the request before component unmounts
controller.abort();
};
}, []);
useEffect(() => { const controller = new AbortController(); const signal = controller.signal; fetch(API, { signal: signal }) .then((response) => response.json()) .then((response) => { // handle success console.log(response); }) .catch((err) => { if (err.name === 'AbortError') { console.log('successfully aborted'); } else { // handle error } }); return () => { // cancel the request before component unmounts controller.abort(); }; }, []);
useEffect(() => {
  const controller = new AbortController();
  const signal = controller.signal;

   fetch(API, {
      signal: signal
    })
    .then((response) => response.json())
    .then((response) => {
      // handle success
      console.log(response);
    })
    .catch((err) => {
      if (err.name === 'AbortError') {
        console.log('successfully aborted');
      } else {
        // handle error
      }
    });
  return () => {
    // cancel the request before component unmounts
    controller.abort();
  };
}, []);

حالا، حتی اگر کاربر پیش از دریافت نتیجه، صفحه را ترک کند یا به مسیر دیگری هدایت شود، دیگر هشداری دریافت نخواهیم کرد؛ زیرا درخواست قبل از unmount شدن کامپوننت، لغو شده و از به‌روزرسانی state جلوگیری می‌شود.

لغو درخواست با استفاده از Axios cancel token

برای پیاده‌سازی همین رویکرد با Axios، ابتدا

CancelToken.source()
CancelToken.source() را در متغیری مثل source ذخیره می‌کنیم. سپس این توکن را به‌عنوان یکی از گزینه‌های Axios به درخواست اضافه کرده و در صورت نیاز، می‌توانیم با
source.cancel()
source.cancel() آن را لغو کنیم.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
useEffect(() => {
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios
.get(API, {
cancelToken: source.token
})
.catch((err) => {
if (axios.isCancel(err)) {
console.log('successfully aborted');
} else {
// handle error
}
});
return () => {
// cancel the request before component unmounts
source.cancel();
};
}, []);
useEffect(() => { const CancelToken = axios.CancelToken; const source = CancelToken.source(); axios .get(API, { cancelToken: source.token }) .catch((err) => { if (axios.isCancel(err)) { console.log('successfully aborted'); } else { // handle error } }); return () => { // cancel the request before component unmounts source.cancel(); }; }, []);
useEffect(() => {
  const CancelToken = axios.CancelToken;
  const source = CancelToken.source();
  axios
    .get(API, {
      cancelToken: source.token
    })
    .catch((err) => {
      if (axios.isCancel(err)) {
        console.log('successfully aborted');
      } else {
        // handle error
      }
    });
  return () => {
    // cancel the request before component unmounts
    source.cancel();
  };
}, []);

مشابه

AbortError
AbortError در
AbortController
AbortController، در Axios نیز می‌توانیم با استفاده از متد
isCancel
isCancel بررسی کنیم که علت خطا لغو دستی بوده یا خیر. اگر خطا به دلیل لغو باشد، نباید state را به‌روزرسانی کنیم.

باید به این نکته توجه داشته باشیم که API مربوط به

CancelToken
CancelToken در Axios منسوخ شده و صرفاً جهت پشتیبانی از پروژه‌های قدیمی ذکر می‌شود. برای پروژه‌های جدید، توصیه می‌شود از
AbortController
AbortController استفاده کنیم.

نحوه استفاده از تابع cleanup در هوک useEffect

برای درک بهتر استفاده از تابع cleanup، یک مثال عملی را بررسی می‌کنیم. فرض کنید دو فایل داریم:

Post
Post و
App
App.

در فایل

Post
Post، اطلاعات مربوط به پست‌ها هنگام mount شدن کامپوننت دریافت می‌شود و در صورت بروز خطا نیز مدیریت مناسبی انجام می‌گیرد. سپس این کامپوننت را در فایل
App
App import کرده و با کلیک روی یک دکمه، کامپوننت را mount و unmount می‌کنیم.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Post component
import React, { useState, useEffect } from "react";
export default function Post() {
const [posts, setPosts] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
fetch("https://jsonplaceholder.typicode.com/posts", { signal: signal })
.then((res) => res.json())
.then((res) => setPosts(res))
.catch((err) => setError(err));
}, []);
return (
<div>
{!error ? (
posts.map((post) => (
<ul key={post.id}>
<li>{post.title}</li>
</ul>
))
) : (
<p>{error}</p>
)}
</div>
);
}
// Post component import React, { useState, useEffect } from "react"; export default function Post() { const [posts, setPosts] = useState([]); const [error, setError] = useState(null); useEffect(() => { const controller = new AbortController(); const signal = controller.signal; fetch("https://jsonplaceholder.typicode.com/posts", { signal: signal }) .then((res) => res.json()) .then((res) => setPosts(res)) .catch((err) => setError(err)); }, []); return ( <div> {!error ? ( posts.map((post) => ( <ul key={post.id}> <li>{post.title}</li> </ul> )) ) : ( <p>{error}</p> )} </div> ); }
// Post component

import React, { useState, useEffect } from "react";
export default function Post() {
  const [posts, setPosts] = useState([]);
  const [error, setError] = useState(null);
  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;
    fetch("https://jsonplaceholder.typicode.com/posts", { signal: signal })
      .then((res) => res.json())
      .then((res) => setPosts(res))
      .catch((err) => setError(err));
  }, []);
  return (
    <div>
      {!error ? (
        posts.map((post) => (
          <ul key={post.id}>
            <li>{post.title}</li>
          </ul>
        ))
      ) : (
        <p>{error}</p>
      )}
    </div>
  );
}
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// App component
import React, { useState } from "react";
import Post from "./Post";
const App = () => {
const [show, setShow] = useState(false);
const showPost = () => {
// toggles posts onclick of button
setShow(!show);
};
return (
<div>
<button onClick={showPost}>Show Posts</button>
{show && <Post />}
</div>
);
};
export default App;
// App component import React, { useState } from "react"; import Post from "./Post"; const App = () => { const [show, setShow] = useState(false); const showPost = () => { // toggles posts onclick of button setShow(!show); }; return ( <div> <button onClick={showPost}>Show Posts</button> {show && <Post />} </div> ); }; export default App;
// App component

import React, { useState } from "react";
import Post from "./Post";
const App = () => {
  const [show, setShow] = useState(false);
  const showPost = () => {
    // toggles posts onclick of button
    setShow(!show);
  };
  return (
    <div>
      <button onClick={showPost}>Show Posts</button>
      {show && <Post />}
    </div>
  );
};
export default App;

اگر کاربر روی دکمه کلیک کند و قبل از نمایش پست‌ها دوباره روی آن کلیک نماید، یعنی پیش از دریافت پاسخ، کامپوننت

Post
Post از DOM حذف شود، در نسخه‌های ۱۷ به پایین React ممکن است هشدار زیر در کنسول نمایش داده شود:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
at Post (https://luzwr.csb.app/src/Post.js:26:41)
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. at Post (https://luzwr.csb.app/src/Post.js:26:41)
Warning: Can't perform a React state update on an unmounted component. 
This is a no-op, but it indicates a memory leak in your application. 
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
    at Post (https://luzwr.csb.app/src/Post.js:26:41)

دلیل این هشدار این است که useEffect همچنان در حال اجراست و پس از دریافت پاسخ API، قصد به‌روزرسانی state کامپوننتی را دارد که دیگر در DOM موجود نیست.

استفاده از AbortController برای حذف هشدار

برای حذف این هشدار، کافی است از

AbortController
AbortController در تابع cleanup استفاده کنیم. به این صورت که پیش از unmount شدن، درخواست fetch را لغو کرده و مانع به‌روزرسانی state شویم.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Post component
import React, { useState, useEffect } from "react";
export default function Post() {
const [posts, setPosts] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
fetch("https://jsonplaceholder.typicode.com/posts", { signal: signal })
.then((res) => res.json())
.then((res) => setPosts(res))
.catch((err) => {
setError(err);
});
return () => controller.abort(); // clean up function
}, []);
return (
<div>
{!error ? (
posts.map((post) => (
<ul key={post.id}>
<li>{post.title}</li>
</ul>
))
) : (
<p>{error}</p>
)}
</div>
);
}
// Post component import React, { useState, useEffect } from "react"; export default function Post() { const [posts, setPosts] = useState([]); const [error, setError] = useState(null); useEffect(() => { const controller = new AbortController(); const signal = controller.signal; fetch("https://jsonplaceholder.typicode.com/posts", { signal: signal }) .then((res) => res.json()) .then((res) => setPosts(res)) .catch((err) => { setError(err); }); return () => controller.abort(); // clean up function }, []); return ( <div> {!error ? ( posts.map((post) => ( <ul key={post.id}> <li>{post.title}</li> </ul> )) ) : ( <p>{error}</p> )} </div> ); }
// Post component

import React, { useState, useEffect } from "react";
export default function Post() {
  const [posts, setPosts] = useState([]);
  const [error, setError] = useState(null);
  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;
    fetch("https://jsonplaceholder.typicode.com/posts", { signal: signal })
      .then((res) => res.json())
      .then((res) => setPosts(res))
      .catch((err) => {
        setError(err);
      });
    return () => controller.abort(); // clean up function
  }, []);
  return (
    <div>
      {!error ? (
        posts.map((post) => (
          <ul key={post.id}>
            <li>{post.title}</li>
          </ul>
        ))
      ) : (
        <p>{error}</p>
      )}
    </div>
  );
}

ما حتی پس از لغو با

abort()
abort()، ممکن است همچنان هشدار را در کنسول مشاهده نماییم. دلیل این امر، تلاش برای به‌روزرسانی state در داخل بلاک
catch
catch پس از بروز
AbortError
AbortError است. برای جلوگیری از این هشدار، در داخل
catch
catch باید بررسی کنیم که آیا خطا از نوع
AbortError
AbortError بوده یا خیر، و در این صورت از به‌روزرسانی state صرف‌نظر کنیم.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Post component
import React, { useState, useEffect } from "react";
export default function Post() {
const [posts, setPosts] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
fetch("https://jsonplaceholder.typicode.com/posts", { signal: signal })
.then((res) => res.json())
.then((res) => setPosts(res))
.catch((err) => {
if (err.name === "AbortError") {
console.log("successfully aborted");
} else {
setError(err);
}
});
return () => controller.abort();
}, []);
return (
<div>
{!error ? (
posts.map((post) => (
<ul key={post.id}>
<li>{post.title}</li>
</ul>
))
) : (
<p>{error}</p>
)}
</div>
);
}
// Post component import React, { useState, useEffect } from "react"; export default function Post() { const [posts, setPosts] = useState([]); const [error, setError] = useState(null); useEffect(() => { const controller = new AbortController(); const signal = controller.signal; fetch("https://jsonplaceholder.typicode.com/posts", { signal: signal }) .then((res) => res.json()) .then((res) => setPosts(res)) .catch((err) => { if (err.name === "AbortError") { console.log("successfully aborted"); } else { setError(err); } }); return () => controller.abort(); }, []); return ( <div> {!error ? ( posts.map((post) => ( <ul key={post.id}> <li>{post.title}</li> </ul> )) ) : ( <p>{error}</p> )} </div> ); }
// Post component

import React, { useState, useEffect } from "react";

export default function Post() {
  const [posts, setPosts] = useState([]);
  const [error, setError] = useState(null);
  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;

      fetch("https://jsonplaceholder.typicode.com/posts", { signal: signal })
      .then((res) => res.json())
      .then((res) => setPosts(res))
      .catch((err) => {
        if (err.name === "AbortError") {
          console.log("successfully aborted");
        } else {
          setError(err);
        }
      });
    return () => controller.abort();
  }, []);
  return (
    <div>
      {!error ? (
        posts.map((post) => (
          <ul key={post.id}>
            <li>{post.title}</li>
          </ul>
        ))
      ) : (
        <p>{error}</p>
      )}
    </div>
  );
}

نکته مهم: هنگام استفاده از Fetch API باید بررسی

err.name === "AbortError"
err.name === "AbortError" انجام شود، و اگر از Axios استفاده می‌کنیم، باید از متد
axios.isCancel()
axios.isCancel() استفاده نماییم.

با این توضیحات، اکنون ما به‌خوبی می‌دانیم چطور باید برای جلوگیری از memory leak، خطاهای ناخواسته و هشدارهای کنسول از تابع cleanup در هوک useEffect استفاده نماییم.

مدیریت فراخوانی‌های غیرمنتظره useEffect

هوک useEffect در React برای مدیریت side effectها بسیار مفید است. با این حال، گاهی اوقات ممکن است رفتاری غیرمنتظره از آن مشاهده شود. این رفتار معمولاً ناشی از استفاده نادرست، حذف وابستگی‌های لازم یا پیاده‌سازی نامناسب تابع cleanup است.

بنابراین، اگر با شرایطی مواجه شدیم که useEffect ما رفتار عجیبی داشت، اقدامات زیر می‌توانند کمک‌کننده باشند:

چه زمانی نیازی به تابع cleanup نیست؟

در این راهنما دیدیم که چگونه می‌توانیم با استفاده از تابع cleanup در useEffect از memory leak جلوگیری کرده و عملکرد برنامه را بهبود بخشیم. اما در برخی موارد، استفاده از این تابع الزامی نیست.

برای مثال، اگر useEffect ما دارای شرایط زیر باشد، ممکن است نیازی به تابع cleanup نداشته باشیم:

به عنوان مثال:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import { useEffect } from 'react';
function Page({ title }) {
useEffect(() => {
document.title = title;
}, [title]);
return <h1>{title}</h1>;
}
import { useEffect } from 'react'; function Page({ title }) { useEffect(() => { document.title = title; }, [title]); return <h1>{title}</h1>; }
import { useEffect } from 'react';

function Page({ title }) {
  useEffect(() => {
    document.title = title;
  }, [title]);

  return <h1>{title}</h1>;   
}

در کد بالا، با اینکه یک اثر اعمال شده، اما نیازی به تابع cleanup نیست. این اثر مستقل است، side effect بیرونی ندارد و وابستگی آن (

title
title) به‌درستی در آرایه آمده است. علاوه بر این، اگر استفاده از useEffect واقعاً ضروری نیست، بهتر است از سایر هوک‌های مناسب‌تر استفاده شود.

در اغلب مواقع، از useEffect برای تعامل با دنیای بیرون از React استفاده می‌کنیم، به‌طوری‌که این تعامل باعث اختلال در سیستم رندر React نشود و عملکرد را کاهش ندهد.

جمع‌بندی

هوک useEffect در React ابزار ارزشمندی برای مدیریت side effectها است، اما استفاده نادرست از آن می‌تواند به رفتارهای غیرمنتظره منجر شود. برای جلوگیری از مسائلی مانند فراخوانی ناخواسته یا memory leakها، لازم است آن را در ساختار منطقی کامپوننت‌ها قرار دهیم (مثلاً قبل یا بعد از توابع دیگر در صورت نیاز) و وابستگی‌ها را به‌درستی مدیریت کنیم.

استفاده مؤثر از توابع cleanup می‌تواند از اثرات ناخواسته جلوگیری کرده و عملکرد برنامه را بهبود دهد. با درک زمان و چرایی فراخوانی useEffect، می‌توانیم lifecycle کامپوننت‌های React را با اطمینان مدیریت کرده و از مشکلات رایج دور بمانیم.