مدیریت State و Effect در Custom Hookها

Custom Hookها معمولاً به این دلیل ساخته می‌شوند که State یا Effectی را که کامپوننت‌ها تکرار می‌کنند، یک‌جا جمع کنند. در این درس می‌بینیم وقتی State و Effect داخل یک Hook قرار می‌گیرند، چه مسئولیت‌هایی به عهده خود Hook است و چه چیزهایی باید در رابط آن دیده شود.

Hook مسئول Effect و Cleanup خودش است

اصول Effect که در فصل ششم دیدیم، داخل Custom Hook هم دقیقاً همان‌طور برقرار است. اگر Hook یک رویداد را شنود می‌کند، تایمر می‌سازد یا به منبع بیرونی وصل می‌شود، خودش باید Cleanup را هم بنویسد. کامپوننتی که از Hook استفاده می‌کند نباید مجبور باشد چیزی درباره پاک‌سازی بداند:

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }

    window.addEventListener("online", handleOnline);
    window.addEventListener("offline", handleOffline);

    return () => {
      window.removeEventListener("online", handleOnline);
      window.removeEventListener("offline", handleOffline);
    };
  }, []);

  return isOnline;
}

هر کامپوننتی که useOnlineStatus() را صدا بزند، به‌محض Mount شدن شنودها را ثبت می‌کند و هنگام Unmount آن‌ها را پاک می‌کند، بدون اینکه خودش کدی برای این کار بنویسد.

پارامترهای Hook، Dependency Effect می‌شوند

اگر Effect داخل Hook از پارامترهای Hook استفاده می‌کند، آن پارامترها باید در Dependency Array باشند. به این ترتیب وقتی کامپوننت مقدار جدیدی می‌دهد، Effect دوباره همگام می‌شود؛ درست مثل هر Effect دیگر. در مثال زیر با تغییر url، Cleanup قبلی اجرا و درخواست تازه‌ای شروع می‌شود:

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let ignore = false;
    setLoading(true);

    fetch(url)
      .then((res) => res.json())
      .then((json) => {
        if (!ignore) {
          setData(json);
          setLoading(false);
        }
      });

    return () => {
      ignore = true;
    };
  }, [url]);

  return { data, loading };
}

پرچم ignore همان راه‌حل Race Condition است که در فصل ششم دیدیم. اگر Hook این محافظت را نداشته باشد، هر کامپوننتی که از آن استفاده کند در معرض همان باگ قرار می‌گیرد؛ یعنی مزیت استخراج به Hook این است که این نکته فقط یک‌بار و درست نوشته می‌شود.

State داخل Hook، ولی مرز روشن با بیرون

هر State داخل Hook خصوصی است و فقط از طریق مقدارهای برگشتی در دسترس قرار می‌گیرد. اگر می‌خواهید کامپوننت بتواند State را تغییر دهد، باید تابعی صریح برگردانید (مثل toggle یا reset)، نه اینکه خود setState را بی‌دلیل بیرون بدهید. این کار جلوی تغییرهای ناخواسته را می‌گیرد و پیاده‌سازی داخلی را قابل تغییر نگه می‌دارد.

مراقب Effectهای پنهان باشید

یک Hook که در ظاهر ساده به نظر می‌رسد ولی داخلش درخواست شبکه یا تغییر document.title انجام می‌دهد، ممکن است برای مصرف‌کننده غافلگیرکننده باشد. نام Hook و مستندات آن باید روشن کند چه کار جانبی انجام می‌دهد: useDocumentTitle نام روشنی است، ولی usePageSetup این را پنهان می‌کند.

همان قاعده «نیاز نداشتن به Effect»

ساختن یک Custom Hook دلیل نمی‌شود که Effect غیرضروری بنویسید. اگر مقداری از روی State یا Props قابل محاسبه است، هنوز باید مستقیم محاسبه شود، نه با Effect. Hook فقط جای‌گذاری کدی است که از قبل لازم بوده؛ چیزی درباره ضرورت آن عوض نمی‌کند.

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

در این مثال، Hook یک درخواست ساختگی را با Cleanup و پرچم ignore مدیریت می‌کند. روی دکمه‌ها به‌سرعت پشت‌سرهم کلیک کنید تا ببینید فقط آخرین نتیجه نمایش داده می‌شود.

useUser با Cleanup و جلوگیری از Race Condition
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

Custom Hook مسئول Effect و Cleanup خودش است، پارامترهایش در Dependency Array قرار می‌گیرند و State داخلی‌اش با مقدارها و توابع صریح در اختیار بیرون گذاشته می‌شود. Effectهای Hook باید در نام و رابط آن قابل‌حدس باشند و Hook نباید بهانه‌ای برای Effectهای غیرضروری شود. با این درس، فصل دهم به پایان می‌رسد و در ادامه به آزمون فصل ۱۰ می‌رویم.