قوانین Hookها (Rules of Hooks)

در درس قبلی دیدیم Hookها چه مشکلی را حل کردند و چرا برای Function Componentها اهمیت پیدا کردند. اما برای این‌که Hookها به‌درستی کار کنند، React به دو قانون ساده اما بسیار مهم نیاز دارد. نادیده گرفتن این قوانین، معمولاً باعث رفتارهای عجیب و سخت‌ردیابی می‌شود.

چرا اصلاً به این قوانین نیاز داریم

React برای هر کامپوننت، ترتیب فراخوانی Hookها را در هر رندر دنبال می‌کند و بر اساس همین ترتیب، مشخص می‌کند کدام State یا Effect متعلق به کدام useState یا useEffect است. React این کار را نه با نام متغیرها، بلکه صرفاً بر اساس ترتیب فراخوانی Hookها انجام می‌دهد. به همین دلیل، اگر ترتیب فراخوانی Hookها بین رندرهای مختلف تغییر کند، React دیگر نمی‌تواند به‌درستی State درست را به هر Hook نسبت دهد.

قانون اول: Hookها فقط در سطح بالای کامپوننت فراخوانی شوند

Hookها نباید داخل شرط، حلقه یا تابع تودرتو فراخوانی شوند؛ باید همیشه مستقیماً در بدنه اصلی Function Component (یا یک Custom Hook) و در سطح بالا نوشته شوند.

// نادرست: Hook داخل شرط
function Profile({ isLoggedIn }) {
  if (isLoggedIn) {
    const [name, setName] = useState("");
  }
  // ...
}

مشکل این کد این است که در برخی رندرها (وقتی isLoggedIn برابر false است)، این useState اصلاً فراخوانی نمی‌شود؛ در نتیجه، اگر Hookهای دیگری بعد از آن در کامپوننت وجود داشته باشند، ترتیب فراخوانی آن‌ها بین رندرهای مختلف عوض می‌شود و React دیگر نمی‌تواند State درست را به هرکدام نسبت دهد.

نسخه درست این است که خود useState همیشه فراخوانی شود، و فقط استفاده از مقدار آن، در صورت نیاز، شرطی باشد:

function Profile({ isLoggedIn }) {
  const [name, setName] = useState("");

  if (!isLoggedIn) {
    return null;
  }

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

همین قانون درباره حلقه و توابع تودرتو هم صدق می‌کند

// نادرست: Hook داخل حلقه
function ItemList({ items }) {
  items.forEach((item) => {
    const [checked, setChecked] = useState(false); // مجاز نیست
  });
}

// نادرست: Hook داخل تابع تودرتو
function Form() {
  function handleClick() {
    const [count, setCount] = useState(0); // مجاز نیست
  }
}

در هر دوی این حالت‌ها، تعداد یا ترتیب فراخوانی Hook می‌تواند بین اجراهای مختلف تغییر کند، که دقیقاً همان چیزی است که React نمی‌تواند مدیریت کند.

قانون دوم: Hookها فقط از Function Componentها یا Custom Hookها فراخوانی شوند

Hookها نباید از یک تابع جاوااسکریپت معمولی (که نه یک کامپوننت است و نه یک Custom Hook) فراخوانی شوند:

// نادرست: تابع معمولی، نه کامپوننت و نه Custom Hook
function getInitialCount() {
  const [count] = useState(0); // مجاز نیست
  return count;
}

دلیل این قانون این است که React فقط می‌تواند فراخوانی Hookها را در بافت (Context اجرایی) یک کامپوننت در حال رندر شدن، یا یک Custom Hook که از داخل چنین کامپوننتی صدا زده شده، ردیابی کند. Custom Hookها که در فصل دهم با جزئیات کامل بررسی خواهیم کرد، طبق قرارداد نام‌گذاری، همیشه با use شروع می‌شوند؛ همین قرارداد نام‌گذاری به ابزارهایی مانند ESLint هم کمک می‌کند تشخیص دهند کدام تابع مجاز به فراخوانی Hook است.

ابزار کمکی: ESLint Plugin مخصوص Hookها

چون رعایت دستی این قوانین در پروژه‌های بزرگ می‌تواند دشوار باشد، تیم React یک افزونه رسمی ESLint به نام eslint-plugin-react-hooks ارائه کرده است که این قوانین را به‌صورت خودکار بررسی می‌کند و در صورت نقض آن‌ها، هشدار یا خطا نمایش می‌دهد. استفاده از این افزونه در پروژه‌های واقعی، به‌شدت توصیه می‌شود.

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

فراخوانی درست Hook در سطح بالای کامپوننت
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

دو قانون اصلی Hookها این است: همیشه در سطح بالای کامپوننت فراخوانی شوند، نه داخل شرط، حلقه یا تابع تودرتو؛ و فقط از داخل Function Componentها یا Custom Hookها فراخوانی شوند. دلیل هر دو قانون یکسان است: React برای تشخیص State هر Hook، به ترتیب ثابت فراخوانی آن‌ها در هر رندر وابسته است. در درس بعدی، مروری کوتاه بر مهم‌ترین Hookهای اصلی React خواهیم داشت.