حلقه‌های بی‌نهایت و مشکلات رایج Dependencyها

در درس‌های قبلی دیدیم که Dependency Array نقش مهمی در کنترل زمان اجرای Effect دارد. در این درس به یکی از رایج‌ترین و در عین حال گیج‌کننده‌ترین اشتباهاتی می‌پردازیم که در کار با Effectها رخ می‌دهد: ایجاد ناخواسته یک حلقه بی‌نهایت از رندرها.

مشکل: Effectی که خودش را دوباره فعال می‌کند

function UserList() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetchUsers().then((data) => {
      setUsers(data);
    });
  }); // بدون Dependency Array

  return <p>{users.length} کاربر</p>;
}

در این کد، چون آرگومان دوم useEffect اصلاً داده نشده، این Effect پس از هر رندر دوباره اجرا می‌شود. اما خود این Effect، با فراخوانی setUsers، باعث یک رندر جدید می‌شود؛ و آن رندر جدید هم دوباره باعث اجرای Effect می‌شود. نتیجه، یک چرخه بی‌پایان از رندر و درخواست به سرور است.

راه‌حل ساده: افزودن Dependency Array مناسب

اگر این درخواست فقط باید یک‌بار، در لحظه Mount شدن کامپوننت انجام شود، کافی است یک آرایه خالی به‌عنوان Dependency اضافه کنیم:

useEffect(() => {
  fetchUsers().then((data) => {
    setUsers(data);
  });
}, []);

مشکل ظریف‌تر: Object یا Array به‌عنوان Dependency

حتی با وجود Dependency Array، ممکن است حلقه بی‌نهایت به شکل ظریف‌تری رخ دهد؛ مخصوصاً وقتی یک Object یا Array که در هر رندر از نو ساخته می‌شود، به‌عنوان Dependency استفاده شود:

function SearchResults({ query }) {
  const options = { query, limit: 10 }; // در هر رندر یک Object جدید ساخته می‌شود

  useEffect(() => {
    fetchResults(options).then(setResults);
  }, [options]); // مشکل: options همیشه Object جدیدی است
}

همان‌طور که در فصل سوم درباره Immutability دیدیم، React مقادیر Dependency Array را با مقایسه ارجاع (Reference Equality) بررسی می‌کند، نه محتوای داخلی. چون options در هر رندر یک Object کاملاً جدید است (حتی اگر محتوایش یکسان باشد)، React همیشه آن را «تغییر‌کرده» تشخیص می‌دهد؛ Effect دوباره اجرا می‌شود، رندر جدیدی رخ می‌دهد، و دوباره یک options جدید ساخته می‌شود. نتیجه، همان حلقه بی‌نهایت است، اما این‌بار پنهان‌تر.

راه‌حل: استفاده از مقادیر Primitive در Dependency Array

رایج‌ترین راه‌حل، استفاده مستقیم از مقادیر ساده (رشته، عدد، Boolean) به‌جای کل Object در Dependency Array است:

function SearchResults({ query }) {
  useEffect(() => {
    fetchResults({ query, limit: 10 }).then(setResults);
  }, [query]); // فقط query که یک رشته ساده است
}

در این نسخه، چون query یک رشته است، مقایسه آن بین رندرها بر اساس مقدار انجام می‌شود، نه ارجاع؛ در نتیجه Effect فقط زمانی دوباره اجرا می‌شود که مقدار واقعی query تغییر کرده باشد.

راه‌حل دیگر: useMemo برای پایدار نگه‌داشتن Object

در مواردی که واقعاً نیاز به یک Object یا Array به‌عنوان Dependency وجود دارد، می‌توان از useMemo استفاده کرد تا آن مقدار، تا وقتی ورودی‌هایش تغییر نکرده، بین رندرها ثابت بماند. جزئیات کامل useMemo و دلیل دقیق کارکرد آن، موضوع فصل سیزدهم است.

چرا این مشکل مهم است

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

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

رفع حلقه بی‌نهایت با Dependency درست
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

حلقه بی‌نهایت در Effectها معمولاً از دو منبع اصلی می‌آید: نبود Dependency Array مناسب برای Effectی که خودش State را تغییر می‌دهد، یا استفاده از یک Object یا Array تازه‌ساخته‌شده به‌عنوان Dependency که هرگز از نظر ارجاع با رندر قبلی برابر نیست. راه‌حل رایج، استفاده از مقادیر Primitive در Dependency Array یا پایدار نگه‌داشتن Objectها با ابزارهایی مانند useMemo است. در درس بعدی به این می‌پردازیم که چه زمانی اصلاً نباید از Effect استفاده کرد.