Sokan Academy

هوک‌ها در ری‌اکت | قسمت سوم : هوک‌های پیشرفته‌تر

هوک‌ها در ری‌اکت | قسمت سوم : هوک‌های پیشرفته‌تر

در مقاله قبلی با پنج هوک پیشرفته و کمتر شناخته‌شده‌ی React آشنا شدیم که هر کدام کاربرد مخصوص به خود را دارند؛ از مدیریت ساده‌تر فرم‌ها با useActionState، رندر با useTransition و تأخیر در آپدیت UI با useDeferredValue گرفته تا کنترل دقیق‌تر ref با useImperativeHandle و تولید ID های یکتا با useId.

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

  • useOptimistic
  • useLayoutEffect
  • useInsertionEffect
  • useDebugValue
  • و هوک‌های سفارشی (Custom Hooks) برای ساخت منطق‌های قابل‌استفاده‌ی مجدد

این هوک‌ها برای مدیریت دقیق‌تر رندر، بهینه‌سازی استایل‌ها، نمایش اطلاعات در DevTools و ساخت الگوهای قابل‌استفاده مجدد طراحی شده‌اند. هدف این هوک‌ها کمک به توسعه‌دهندگان است تا بتوانند کدهای تمیز و کارآمدتری بنویسند و کنترل بیشتری بر رفتار برنامه داشته باشند.

هوک useOptimistic برای به‌روزرسانی UI به صورت لحظه‌ای

ری اکت همیشه در حال پیشرفت و معرفی هوک‌های جدید برای ساده‌تر کردن توسعه است. هوک useOptimistic یکی از همین هوک هاست که در ورژن ۱۹ React معرفی شد. این هوک به شما این امکان را می‌دهد که رابط کاربری (UI) را به‌صورت خوش‌بینانه (optimistic) به‌روزرسانی کنید؛ یعنی تغییرات را بلافاصله در ظاهر نشان دهید، حتی قبل از آن‌که پاسخ نهایی از سرور یا عملیات اصلی دریافت شود. 

نحوه عملکرد هوک useOptimistic در ری‌ اکت

یک مثال قابل لمس از این هوک لایک کردن در اینستاگرام است؛ وقتی اسکرول می کنید و تعدادی پست را لایک می کنید، اما وقتی بعد از چند ثانیه به همان پست باز می گردید لایک حذف شده است! در واقع، آن قلب فقط به‌صورت موقت و خوش‌بینانه نمایش داده شده بود تا کاربر حس کند عملیات انجام شده است. این اتفاق حتی پیش از اینکه سرور تأیید نهایی را بفرستد افتاد.

هوک useOptimistic در React به شما این امکان را می‌دهد که در هنگام انجام یک عملیات ناهمگام (async)، وضعیت متفاوتی را به کاربر نشان دهید. این هوک یک state اولیه را به عنوان ورودی می‌گیرد و یک نسخه‌ی جدید از آن state برمی‌گرداند که می‌تواند در طول اجرای عملیات (مثل درخواست شبکه) موقتاً متفاوت باشد. شما باید تابعی را به آن بدهید که وضعیت فعلی (current state) و ورودی عملیات را دریافت کرده و وضعیت خوش‌بینانه (optimistic state) را برگرداند، یعنی همان حالتی که می‌خواهید تا زمان دریافت پاسخ واقعی، به کاربر نمایش داده شود.

به این وضعیت “خوش‌بینانه” گفته می‌شود، چون معمولاً برای نمایش فوری نتیجه‌ی یک عمل به کاربر استفاده می‌شود، حتی اگر انجام واقعی آن عمل در پس‌زمینه کمی زمان ببرد.

ورودی‌های useOptimistic:

  • state: مقداری است که در ابتدا و زمانی که هیچ عملیاتی در حال اجرا نیست، بازگردانده می‌شود.
  • (updateFn(currentState, optimisticValue: تابعی است که وضعیت فعلی (currentState) و مقدار خوش‌بینانه (optimisticValue) را دریافت کرده و وضعیت جدید را برمی‌گرداند. این تابع باید خالص باشد و تنها ترکیب این دو مقدار را بازگرداند.

خروجی‌های useOptimistic:

  • optimisticState: وضعیت خوش‌بینانه‌ای که در هنگام اجرای یک عملیات async نمایش داده می‌شود. در حالت عادی برابر با state است، اما هنگام انتظار برای پاسخ، مقدار updateFn را نشان می‌دهد.
  • addOptimistic: تابعی برای اعمال به‌روزرسانی خوش‌بینانه که با دریافت یک مقدار (optimisticValue) اجرا می‌شود و رابط کاربری را موقتاً به‌روزرسانی می‌کند.
import { useOptimistic } from 'react';

function AppContainer() {
  const [optimisticState, addOptimistic] = useOptimistic(
    state,
    // تابع به‌روزرسانی (updateFn)
    (currentState, optimisticValue) => {
      // وضعیت فعلی را با مقدار خوش‌بینانه ترکیب کرده
      // و وضعیت جدید را برمی‌گرداند
    }
  );
}

در اینجا:

  • state وضعیت اصلی برنامه است.
  • optimisticState نسخه‌ای از وضعیت است که به‌طور موقت و تا زمان اتمام عملیات async نمایش داده می‌شود.
  • addOptimistic تابعی است که برای اعمال مقدار خوش‌بینانه (مثلاً افزودن آیتم، لایک کردن، حذف موقت و...) استفاده می‌شود.

مثال ساده از فرم: در این مثال، می‌خواهیم نحوه‌ی استفاده از هوک useOptimistic را در یک فرم ساده ارسال پیام بررسی کنیم. هدف این است که کاربر بلافاصله نتیجه‌ی عمل خود را در صفحه ببیند، حتی پیش از آنکه پاسخ واقعی از سرور برسد. در این مثال  تابعی به نام deliverMessage تعریف شده است که ارسال پیام به سرور را شبیه‌سازی می‌کند. در واقع، این تابع فقط با یک تأخیر زمانی کوتاه (مثلاً یک ثانیه) پیام را برمی‌گرداند تا رفتاری مشابه ارسال واقعی به سرور داشته باشد.

export async function deliverMessage(message) {
  await new Promise((res) => setTimeout(res, 1000));
  return message;
}

در کامپوننت App، یک state به نام messages تعریف شده است که لیست پیام‌های موجود را نگه می‌دارد. در ابتدا این لیست شامل یک پیام ساده است. تابع sendMessageAction نیز مسئول ارسال واقعی پیام است. زمانی که این تابع اجرا می‌شود، ابتدا پیام را از فرم دریافت کرده، آن را از طریق تابع deliverMessage ارسال می‌کند و پس از بازگشت نتیجه، پیام جدید را به ابتدای لیست پیام‌ها اضافه می‌کند. برای جلوگیری از کندی یا قفل شدن رابط کاربری در هنگام این فرآیند، از تابع startTransition استفاده شده است که باعث می‌شود عملیات به‌روزرسانی به‌صورت روان و در پس‌زمینه انجام شود.

 کامپوننت Thread، بخش اصلی رابط کاربری را تشکیل می‌دهد. در این بخش، از هوک useOptimistic برای ایجاد حالت موقت پیام‌ها استفاده شده است. این هوک دو مقدار برمی‌گرداند:

  1. optimisticMessages که نسخه‌ی موقتی از لیست پیام‌ها است.
  2. addOptimisticMessage که تابعی برای افزودن پیام جدید به این لیست موقتی محسوب می‌شود.

هر بار که کاربر پیامی در فرم وارد کرده و آن را ارسال می‌کند، تابع formAction اجرا می‌شود. این تابع ابتدا با استفاده از addOptimisticMessage پیام جدید را بدون اینکه منتظر پاسخ سرور بماند، بلافاصله در رابط کاربری نشان می‌دهد و فرم را پاک می‌کند. سپس در پس‌زمینه، تابع sendMessageAction فراخوانی می‌شود تا پیام به‌صورت واقعی ارسال شود. در طول این زمان، پیام جدید در صفحه با برچسب “(Sending...)” نمایش داده می‌شود تا به کاربر اطلاع دهد پیام در حال ارسال است. پس از دریافت پاسخ از سرور، پیام واقعی جایگزین نسخه‌ی موقت شده و برچسب حذف می‌شود.

import { useOptimistic, useState, useRef, startTransition } from "react";
import { deliverMessage } from "./actions.js";

function Thread({ messages, sendMessageAction }) {
  const formRef = useRef();
  function formAction(formData) {
    addOptimisticMessage(formData.get("message"));
    formRef.current.reset();
    startTransition(async () => {
      await sendMessageAction(formData);
    });
  }
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [
      {
        text: newMessage,
        sending: true
      },
      ...state,
    ]
  );

  return (
    <>
      <form action={formAction} ref={formRef}>
        <input type="text" name="message" placeholder="Hello!" />
        <button type="submit">Send</button>
      </form>
      {optimisticMessages.map((message, index) => (
        <div key={index}>
          {message.text}
          {!!message.sending && <small> (Sending...)</small>}
        </div>
      ))}
      
    </>
  );
}

export default function App() {
  const [messages, setMessages] = useState([
    { text: "Hello there!", sending: false, key: 1 }
  ]);
  async function sendMessageAction(formData) {
    const sentMessage = await deliverMessage(formData.get("message"));
    startTransition(() => {
      setMessages((messages) => [{ text: sentMessage }, ...messages]);
    })
  }
  return <Thread messages={messages} sendMessageAction={sendMessageAction} />;
}

در مجموع، این مثال نشان می‌دهد که چطور می‌توان با استفاده از هوک useOptimistic تجربه‌ی کاربر را بهبود داد. این روش مخصوصاً در برنامه‌هایی که با فرم‌های تعاملی سروکار دارند، بسیار کاربردی است.

موارد استفاده از useOptimistic:

به‌روزرسانی فرم ها: کاربرد هوک useOptimistic در فرم‌ها این است که رابط کاربری را به‌صورت خوش‌بینانه (Optimistic) به‌روزرسانی می‌کند؛ یعنی قبل از اینکه عملیات واقعی (مثل درخواست به سرور) کامل شود، نتیجهٔ مورد انتظار را موقتاً در رابط کاربری نمایش می‌دهد تا برنامه سریع‌تر و روان‌تر به نظر برسد. 

هوک useLayoutEffect برای اندازه‌گیری ابعاد عناصر قبل از رندر شدن مرورگر

هوک useLayoutEffect می‌تواند عملکرد برنامه را کاهش دهد. تا حد ممکن از useEffect به‌جای آن استفاده کنید.
هوک useLayoutEffect نسخه‌ای از useEffect است که قبل از آن‌که مرورگر صفحه را مجدداً ترسیم (repaint) کند اجرا می‌شود. این تفاوت زمانی اجرا، دلیل اصلی کاربرد متفاوت آن در سناریوهای خاص است. ساختار کلی آن به شکل زیر است:

useLayoutEffect(setup, dependencies?)

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

import { useState, useRef, useLayoutEffect } from 'react';

function Tooltip() {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0);

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height);
  }, []);
  // ...

در این مثال، تابع داخل useLayoutEffect درست پس از به‌روزرسانی DOM و قبل از نمایش نهایی صفحه اجرا می‌شود. با فراخوانی متد getBoundingClientRect، ارتفاع عنصر tooltip اندازه‌گیری می‌شود و سپس در state ذخیره می‌گردد. به این ترتیب، می‌توان موقعیت یا ابعاد tooltip را به‌درستی قبل از نمایش روی صفحه محاسبه کرد و از پرش‌های ناگهانی یا تغییر ناگهانی اندازه جلوگیری نمود.

ورودی های useLayoutEffect:

  • Setup:این پارامتر تابعی است که منطق اصلی افکت را در بر دارد و قبل از ترسیم مجدد صفحه (repaint) اجرا می‌شود. درون آن می‌توانید کدهایی بنویسید که نیاز به اجرای فوری پس از به‌روزرسانی DOM دارند، مانند اندازه‌گیری یا هماهنگی عناصر. این تابع می‌تواند یک تابع پاک‌سازی (cleanup) بازگرداند تا React هنگام تغییر وابستگی‌ها یا حذف کامپوننت، آن را اجرا کند.
  • dependencies (اختیاری): این بخش شامل مقادیر واکنشی (مثل props، state یا متغیرهایی که درون کامپوننت تعریف کرده‌اید) است که در تابع setup از آن‌ها استفاده می‌کنید. با مشخص کردن وابستگی‌ها، به ری‌اکت می‌گویید که چه زمانی لازم است افکت دوباره اجرا شود. دقت کنید که لیست وابستگی‌ها باید به‌صورت ثابت و مشخص، مثل [dep1, dep2, dep3] نوشته شود. اگر وابستگی‌ها را مشخص نکنید، افکت شما بعد از هر بار رندر کامپوننت دوباره اجرا می‌شود.

خروجی‌ useLayoutEffect:

  • هوک useLayoutEffect هیچ مقداری بازنمی‌گرداند و خروجی آن undefined است.

فرض کنید کامپوننتی مثل Tooltip دارید که هنگام رفتن ماوس روی یک دکمه ظاهر می‌شود. اگر فضای کافی در بالای دکمه وجود داشته باشد، باید در بالا نمایش داده شود، و اگر نه، در پایین. برای تصمیم‌گیری درباره‌ی محل نهایی تولتیپ، باید قبل از نمایش صفحه ارتفاع آن را بدانیم.برای تصمیم‌گیری درست درباره‌ی محل نمایش، باید پیش از ترسیم صفحه ارتفاع واقعی تولتیپ را بدانید تا بتوانید موقعیت صحیح را مشخص کنید. برای انجام این کار، لازم است رندر را در دو مرحله انجام دهید:

  1. ابتدا تولتیپ را موقتاً در هر جایی (حتی در موقعیت نادرست) رندر کنید. سپس ارتفاع آن را اندازه‌گیری کرده و تصمیم بگیرید در کجا باید قرار گیرد.
  2. در نهایت، تولتیپ را دوباره در موقعیت درست رندر کنید.

تمام این مراحل باید پیش از آن‌که مرورگر صفحه را مجدداً ترسیم کند انجام شوند تا کاربر حرکت یا پرش تولتیپ را نبیند. 

TooltipContainer.js:


 return (
    <div
      style={{
        position: 'absolute',
        pointerEvents: 'none',
        left: 0,
        top: 0,
        transform: `translate3d(${x}px, ${y}px, 0)`
      }}
    >
      <div ref={contentRef} className="tooltip">
        {children}
      </div>
    </div>
  );
}

Tooltip.js:

import { useRef, useLayoutEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import TooltipContainer from './TooltipContainer.js';

export default function Tooltip({ children, targetRect }) {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0);

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height);
    console.log('Measured tooltip height: ' + height);
  }, []);

  let tooltipX = 0;
  let tooltipY = 0;
  if (targetRect !== null) {
    tooltipX = targetRect.left;
    tooltipY = targetRect.top - tooltipHeight;
    if (tooltipY < 0) {
      // It doesn't fit above, so place below.
      tooltipY = targetRect.bottom;
    }
  }

  return createPortal(
    <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>
      {children}
    </TooltipContainer>,
    document.body
  );
}

ButtonWithTooltip.js:

import { useState, useRef } from 'react';
import Tooltip from './Tooltip.js';

export default function ButtonWithTooltip({ tooltipContent, ...rest }) {
  const [targetRect, setTargetRect] = useState(null);
  const buttonRef = useRef(null);
  return (
    <>
      <button
        {...rest}
        ref={buttonRef}
        onPointerEnter={() => {
          const rect = buttonRef.current.getBoundingClientRect();
          setTargetRect({
            left: rect.left,
            top: rect.top,
            right: rect.right,
            bottom: rect.bottom,
          });
        }}
        onPointerLeave={() => {
          setTargetRect(null);
        }}
      />
      {targetRect !== null && (
        <Tooltip targetRect={targetRect}>
          {tooltipContent}
        </Tooltip>
      )
    }
    </>
  );
}

App.js:

import ButtonWithTooltip from './ButtonWithTooltip.js';

export default function App() {
  return (
    <div>
      <ButtonWithTooltip
        tooltipContent={
          <div>
            This tooltip does not fit above the button.
            <br />
            This is why it's displayed below instead!
          </div>
        }
      >
        Hover over me (tooltip above)
      </ButtonWithTooltip>
      <div style={{ height: 50 }} />
      <ButtonWithTooltip
        tooltipContent={
          <div>This tooltip fits above the button</div>
        }
      >
        Hover over me (tooltip below)
      </ButtonWithTooltip>
      <div style={{ height: 50 }} />
      <ButtonWithTooltip
        tooltipContent={
          <div>This tooltip fits above the button</div>
        }
      >
        Hover over me (tooltip below)
      </ButtonWithTooltip>
    </div>
  );
}

این کد مرحله‌به‌مرحله این‌طور عمل می‌کند:

  1. رندر اولیه با ارتفاع: کامپوننت Tooltip برای اولین بار با tooltipHeight = 0 رندر می‌شود؛ بنابراین ممکن است در جای نامناسبی قرار بگیرد (چون هنوز ارتفاع واقعی را نمی‌دانیم).
  2. به‌روزرسانی DOM و اجرای useLayoutEffect: ری‌اکت خروجی JSX را در DOM قرار می‌دهد و بلافاصله ــ قبل از اینکه مرورگر صفحه را نمایش دهد (paint) ــ کد داخل useLayoutEffect را اجرا می‌کند.
  3. اندازه‌گیری ارتفاع و درخواست رندر مجدد فوری: در useLayoutEffect با getBoundingClientRect() ارتفاع واقعی محتوا اندازه‌گیری می‌شود و با setTooltipHeight، state به‌روزرسانی می‌گردد؛ این کار فوراً یک رندر مجدد را کلید می‌زند.
  4. رندر دوم با ارتفاع واقعی: Tooltip دوباره رندر می‌شود، این بار با tooltipHeight واقعی؛ حالا می‌توان موقعیت صحیح (بالا/پایین) را دقیق و بدون حدس تعیین کرد.
  5. به‌روزرسانی DOM و نمایش نهایی بدون پرش: ری‌اکت DOM را با وضعیت درست به‌روزرسانی می‌کند و سپس مرورگر Tooltip را نمایش می‌دهد؛ چون همهٔ اندازه‌گیری‌ها و جابه‌جایی‌ها قبل از paint انجام شده، کاربر هیچ پرش یا حرکت ناگهانی نمی‌بیند.

نکته اینجاست که با وجود این‌که کامپوننت Tooltip باید در دو مرحله رندر شود کاربر فقط نتیجه‌ی نهایی را می‌بیند. به‌عبارت دیگر، مرحله‌ی موقتی (که در آن ارتفاع هنوز مشخص نیست) هیچ‌گاه به‌صورت بصری نمایش داده نمی‌شود، چون همه‌ی این محاسبات و رندر مجدد پیش از آن‌که مرورگر صفحه را ترسیم کند انجام می‌شود. به همین دلیل است که در این مثال باید از useLayoutEffect استفاده کنید نه از useEffect!

تفاوت useLayoutEffect با useEffect:

تفاوت useLayoutEffect با useEffect در زمان اجرای آن‌هاست. useLayoutEffect تضمین می‌کند که تمام اندازه‌گیری‌ها و تغییرات قبل از نمایش نهایی صفحه انجام شوند، در حالی که useEffect پس از ترسیم صفحه اجرا می‌شود و ممکن است باعث پرش یا تغییر ناگهانی در رابط کاربری شود.

تفاوت useLayoutEffect با useEffect

موارد استفاده از useLayoutEffect:

هوک useLayoutEffect زمانی استفاده می‌شود که بخواهیم قبل از آن‌که مرورگر صفحه را مجدداً ترسیم (repaint) کند، محاسبات مربوط به چیدمان (layout) را انجام دهیم. در حالت عادی، بیشتر کامپوننت‌ها فقط JSX را برمی‌گردانند و مرورگر موقعیت و اندازه‌ی عناصر را محاسبه کرده و صفحه را نمایش می‌دهد. اما در برخی موارد، لازم است که بدانیم عنصر دقیقاً چه اندازه یا موقعیتی دارد تا بتوانیم قبل از نمایش نهایی، آن را در مکان درست قرار دهیم.

هوک useInsertionEffect برای تزریق استایل‌ها به DOM

این هوک در اصل برای توسعه‌دهندگان کتابخانه‌های CSS-in-JS طراحی شده است. اگر در حال ساخت یک اپلیکیشن معمولی هستید، بهتر است از useEffect یا useLayoutEffect استفاده کنید.
هوک useInsertionEffect به شما امکان می‌دهد پیش از اجرای هر نوع افکت مرتبط با چیدمان (layout effects)، عناصر یا استایل‌ها را در DOM وارد کنید.

useInsertionEffect(setup, dependencies?)

برای مثال، در کتابخانه‌های CSS-in-JS می‌توان از این هوک برای تزریق تگ‌های <style> پیش از اجرای layout effects استفاده کرد:

import { useInsertionEffect } from 'react';

// Inside your CSS-in-JS library
function useCSS(rule) {
  useInsertionEffect(() => {
    // ... inject <style> tags here ...
  });
  return rule;
}

ورودی های useInsertionEffect:

  • Setup: این پارامتر تابعی است که منطق اصلی افکت شما را در بر دارد. می‌توانید درون آن کدهایی بنویسید که باید درست قبل از اجرای هر نوع layout effect اجرا شوند. همچنین می‌توانید از این تابع، یک تابع پاک‌سازی (cleanup) هم برگردانید تا ری‌اکت هنگام تغییر یا حذف کامپوننت، آن را اجرا کند.
    زمانی که کامپوننت برای نخستین‌بار به DOM اضافه می‌شود، ری‌اکت تابع setup را اجرا می‌کند. اگر وابستگی‌ها (dependencies) تغییر کنند، ابتدا تابع پاک‌سازی قبلی اجرا می‌شود و سپس setup جدید با مقادیر تازه فراخوانی می‌گردد. در نهایت، هنگامی که کامپوننت از DOM حذف شود، تابع پاک‌سازی آخرین بار اجرا خواهد شد.
  • dependencies (اختیاری): این بخش شامل مقادیر واکنشی (مثل props، state یا متغیرهایی که درون کامپوننت تعریف کرده‌اید) است که در تابع setup از آن‌ها استفاده می‌کنید. با مشخص کردن وابستگی‌ها، به ری‌اکت می‌گویید که چه زمانی لازم است افکت دوباره اجرا شود. دقت کنید که لیست وابستگی‌ها باید به‌صورت ثابت و مشخص، مثل [dep1, dep2, dep3] نوشته شود. اگر وابستگی‌ها را مشخص نکنید، افکت شما بعد از هر بار رندر کامپوننت دوباره اجرا می‌شود.

خروجی‌ useInsertionEffect:

  • هوک useInsertionEffect هیچ مقداری برنمی‌گرداند و خروجی آن undefined است.

در توسعه‌ی React معمولاً از فایل‌های CSS جداگانه استفاده می‌شود، اما برخی ترجیح می‌دهند استایل‌ها را درون کد جاوااسکریپت بنویسند تا بتوانند آن را با منطق کامپوننت ترکیب کنند. این روش با کمک کتابخانه‌های CSS-in-JS مانند Styled Components یا Emotion انجام می‌شود.
سه روش اصلی در این زمینه وجود دارد:

  1. استخراج استایل‌ها به فایل‌های CSS هنگام build
  2. استفاده از استایل‌های درون‌خطی (inline styles)
  3. تزریق تگ‌های <style> در زمان اجرا

پیشنهاد می‌شود از ترکیب دو روش اول برای بهینه‌سازی استفاده کنید، زیرا تزریق استایل‌ها در زمان اجرا می‌تواند باعث افزایش بار مرورگر و کندی در رندر شود. اگر نیاز به تزریق استایل در زمان اجرا دارید، از useInsertionEffect استفاده کنید تا استایل‌ها پیش از اجرای layout effects وارد DOM شوند و از چشمک‌زدن یا تأخیر جلوگیری شود. این کد نمونه‌ای است از نحوه‌ی استفاده از useInsertionEffect در یک کتابخانه‌ی CSS-in-JS برای تزریق استایل‌ها در زمان اجرا:


function useCSS(rule) {
 useInsertionEffect(() => {
   if (!isInserted.has(rule)) {
     isInserted.add(rule);
     document.head.appendChild(getStyleForRule(rule));
   }
 });
 return rule;
}

function Button() {
 const className = useCSS('...');
 return <div className={className} />;
}

در این مثال، تابع useCSS بررسی می‌کند که آیا قانون CSS قبلاً اضافه شده است یا نه. اگر نه، با استفاده از useInsertionEffect یک تگ <style> در <head> ایجاد می‌کند تا استایل‌ها پیش از layout effects در صفحه اعمال شوند و از تأخیر بصری جلوگیری شود.
به طور کلی، هوک useInsertionEffect ابزاری تخصصی در React است که برای کنترل دقیق زمان تزریق استایل‌ها یا تغییرات DOM طراحی شده است. استفاده‌ی آگاهانه از آن به بهبود عملکرد و پایداری کامپوننت‌ها کمک می‌کند

نکات مهم:

  • هوک useInsertionEffect شبیه به useEffect است، اما از نظر زمان‌بندی اجرا متفاوت عمل می کند.
  • استفاده‌ی بیش از حد از این هوک ممکن است روی عملکرد برنامه (performance) تأثیر منفی بگذارد، پس باید با دقت استفاده شود.
  • اگر نیاز دارید افکت‌ها پس از تکمیل چیدمان (layout) و به‌صورت هم‌زمان اجرا شوند، بهتر است از  useLayoutEffect (که در مقالات قبلی معرفی شد) استفاده کنید.

موارد استفاده از useInsertionEffect:

  • تزریق استایل‌ها در کتابخانه‌های CSS-in-JS: برای وارد کردن استایل‌های پویا به صفحه قبل از اجرای افکت‌های دیگر، به‌ویژه در کتابخانه‌هایی مثل Styled Components یا Emotion.
  • هماهنگی با کتابخانه‌های شخص ثالث: اگر کتابخانه‌ای نیاز دارد که DOM پیش از اجرا آماده باشد، useInsertionEffect اطمینان می‌دهد کد شما در زمان مناسب و پیش از سایر افکت‌ها اجرا شود.

هوک useDebugValue برای نمایش بهتر وضعیت داخلی هوک‌های سفارشی

هوک useDebugValue یکی از هوک‌های کمتر شناخته‌شده‌ی React است که برای نمایش اطلاعات کمکی درباره‌ی یک هوک سفارشی در React DevTools استفاده می‌شود. این هوک فقط در زمان اشکال‌زدایی (debugging) کاربرد دارد و به توسعه‌دهنده کمک می‌کند تا وضعیت داخلی یک هوک را راحت‌تر مشاهده کند. 

برای مثال، فرض کنید هوکی نوشته‌اید که وضعیت آنلاین یا آفلاین بودن کاربر را بررسی می‌کند. اگر بخواهید در DevTools به‌جای نمایش مقدار بولی (true یا false)، عبارت قابل‌خواندن‌تری مثل “Online” یا “Offline” نمایش داده شود، می‌توانید از useDebugValue استفاده کنید:

import { useDebugValue } from 'react';

function useOnlineStatus() {
  // ...
  useDebugValue(isOnline ? 'Online' : 'Offline');
  // ...
}

هوک useDebugValue باید در بالاترین سطح از یک هوک سفارشی (Custom Hook) فراخوانی شود تا مقدار قابل‌خواندنی برای اشکال‌زدایی در React DevTools نمایش داده شود. این مقدار به توسعه‌دهنده کمک می‌کند تا هنگام بررسی کامپوننت‌ها، وضعیت داخلی هوک را به‌صورت ساده و واضح مشاهده کند.

ورودی های useDebugValue:

  • value: مقداری است که می‌خواهید در React DevTools نمایش داده شود. این مقدار می‌تواند رشته (string)، عدد، بولین، یا حتی یک شیء باشد.
  • format (اختیاری): تابعی برای قالب‌بندی مقدار نمایش‌داده‌شده در DevTools است. زمانی که کامپوننت در DevTools باز (inspect) می‌شود، React این تابع را صدا می‌زند و مقدار اصلی (value) را به آن می‌دهد. خروجی این تابع، همان مقداری است که در DevTools نمایش داده خواهد شد.اگر این پارامتر را مشخص نکنید، خود مقدار اصلی (value) بدون تغییر نمایش داده می‌شود.

خروجی‌ useDebugValue:

  • useDebugValue هیچ مقداری برنمی‌گرداند.

در مثال زیر یاد می‌گیریم چگونه با استفاده از دو هوک قدرتمند React یعنی useSyncExternalStore و useDebugValue، یک هوک سفارشی (Custom Hook) برای تشخیص وضعیت اینترنت کاربر بسازیم و آن را به‌صورت خوانا در رابط کاربری و ابزار React DevTools نمایش دهیم. ابتدا در هوک useOnlineStatus از useSyncExternalStore استفاده می‌کنیم تا کامپوننت را با تغییرات وضعیت اتصال اینترنت هماهنگ کنیم. این هوک به سه ورودی نیاز دارد:

  1. تابع subscribe که تابع رویدادهای مرورگر را برای تغییر وضعیت اینترنت (رویدادهای online و offline) گوش می‌دهد و زمانی که تغییری رخ دهد، React را از آن باخبر می‌سازد تا دوباره رندر شود.
  2. تابعی که مقدار فعلی را بازمی‌گرداند، یعنی () => navigator.onLine که مشخص می‌کند آیا مرورگر در حال حاضر آنلاین است یا نه.
  3. مقدار پیش‌فرضی برای حالت سرور (SSR)، که در اینجا () => true است تا در محیط‌هایی که navigator در دسترس نیست، برنامه دچار خطا نشود.

نتیجه‌ی useSyncExternalStore در متغیری به نام isOnline ذخیره می‌شود. بعد از آن، از useDebugValue استفاده می‌کنیم تا در ابزار React DevTools وضعیت اتصال را نمایش دهیم:

useDebugValue(isOnline ? 'Online' : 'Offline');

این خط از کد فقط برای نمایش بهتر در زمان debugging است و هیچ تأثیری روی عملکرد برنامه ندارد. در DevTools، وقتی این هوک را بررسی کنید، به‌جای نمایش مقدار خام true یا false، عبارت واضح‌تری مثل "Online" یا "Offline" مشاهده خواهید کرد. 

در نهایت، مقدار isOnline از هوک بازگردانده می‌شود تا بتوان آن را در هر کامپوننت دیگری استفاده کرد. تابع subscribe هم مسئول گوش‌دادن به تغییرات وضعیت اینترنت است؛ هر زمان که کاربر متصل یا قطع شود، این تابع با فراخوانی callback به React اطلاع می‌دهد تا رابط کاربری به‌روز شود.


export function useOnlineStatus() {
  const isOnline = useSyncExternalStore(subscribe, () => navigator.onLine, () => true);
  useDebugValue(isOnline ? 'Online' : 'Offline');
  return isOnline;
}

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

در کامپوننت StatusBar، از هوک useOnlineStatus استفاده کرده‌ایم تا وضعیت اتصال را در رابط کاربری نشان دهیم. اگر کاربر به اینترنت متصل باشد، عبارت ✅ Online نمایش داده می‌شود و در غیر این صورت، پیام ❌ Disconnected ظاهر خواهد شد.

import { useOnlineStatus } from './useOnlineStatus.js';

function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

export default function App() {
  return <StatusBar />;
}

به این ترتیب، ترکیب useSyncExternalStore و useDebugValue به ما اجازه می‌دهد یک هوک سفارشی بسازیم که هم به‌روزرسانی خودکار و واکنشی دارد، هم در DevTools به شکل واضح و حرفه‌ای قابل‌مشاهده است.

موارد استفاده از useDebugValue:

  • نمایش برچسب (Label) در DevTools: هوک useDebugValue برای افزودن برچسب (Label) به هوک‌های سفارشی استفاده می‌شود تا هنگام بررسی کامپوننت‌ها در React DevTools، مقدار یا وضعیت داخلی آن‌ها به شکل خواناتر و قابل‌فهم‌تری نمایش داده شود.
  • به‌تعویق انداختن قالب‌بندی مقدار (Deferring formatting of a debug value): در هوک useDebugValue می‌توانید به‌عنوان آرگومان دوم، یک تابع قالب‌بندی (formatting function) ارسال کنید. این تابع مقدار اصلی را به‌عنوان ورودی دریافت کرده و مقدار قالب‌بندی‌شده‌ای را برمی‌گرداند که در React DevTools نمایش داده می‌شود. این روش باعث می‌شود از اجرای غیرضروری و سنگین محاسبات در هر رندر جلوگیری شود و فقط در زمان نیاز (یعنی هنگام باز بودن DevTools) انجام گیرد. به این ترتیب، عملکرد برنامه بهینه‌تر باقی می‌ماند و در عین حال، در محیط توسعه اطلاعات خواناتری برای اشکال‌زدایی نمایش داده می‌شود.

هوک های سفارشی برای کاهش حجم کد با کمک یک تابع قابل استفاده مجدد

ری‌اکت مجموعه‌ای از هوک‌های از‌پیش‌ساخته‌شده مانند useState، useContext و useEffect را در اختیار شما قرار می‌دهد که بیشتر نیازهای معمول توسعه را برطرف می‌کنند. با این حال، گاهی ممکن است بخواهید برای هدفی خاص‌تر از آن‌ها استفاده کنید. در چنین مواردی ممکن است هوکی با آن عملکرد خاص در خود React وجود نداشته باشد، اما شما می‌توانید با ترکیب هوک‌های موجود، هوک سفارشی (Custom Hook) خود را بسازید.

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

فرض کنید در حال توسعه‌ی یک اپلیکیشن هستید که وابستگی زیادی به اتصال اینترنت دارد. حال می‌خواهید زمانی که کاربر در حال استفاده از برنامه است و اتصال شبکه‌اش قطع می‌شود، به او هشدار دهید. برای انجام این کار، در کامپوننت خود به دو بخش اصلی نیاز دارید:

  • یک state برای نگه‌داری وضعیت شبکه (آنلاین یا آفلاین بودن).
  • یک افکت (Effect) که به رویدادهای سراسری مرورگر (online و offline) گوش دهد و بر اساس آن، state را به‌روزرسانی کند.

پیاده‌سازی اولیه می‌تواند به شکل زیر باشد:

import { useState, useEffect } from 'react';

export default function StatusBar() {
  const [isOnline, setIsOnline] = useState(true);
  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 <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

در این مرحله می‌توانید رفتار کامپوننت را امتحان کنید؛ کافی است اینترنت خود را قطع و وصل کنید تا ببینید چگونه StatusBar بلافاصله با تغییر وضعیت شبکه به‌روزرسانی می‌شود.
حالا فرض کنید می‌خواهید از همین منطق در کامپوننت دیگری نیز استفاده کنید. مثلاً می‌خواهید یک دکمه‌ی Save بسازید که وقتی اتصال شبکه قطع است، غیرفعال شود و به‌جای عبارت "Save"، متن "Reconnecting…" را نمایش دهد. ممکن است ساده‌ترین راه، کپی و جایگذاری (copy-paste) منطق مربوط به isOnline و useEffect از کامپوننت StatusBar در کامپوننت جدید باشد، مانند کد زیر:

import { useState, useEffect } from 'react';

export default function SaveButton() {
  const [isOnline, setIsOnline] = useState(true);
  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);
    };
  }, []);

  function handleSaveClick() {
    console.log('✅ Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

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

بهترین روش برای حل این مشکل، استخراج منطق تکراری در قالب یک هوک سفارشی (Custom Hook) است. فرض کنید ری‌اکت به‌صورت پیش‌فرض هوکی به نام useOnlineStatus داشت؛ در این صورت، می‌توانستید هر دو کامپوننت را بسیار ساده‌تر بنویسید:


const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

function SaveButton() {
  const isOnline = useOnlineStatus();

  function handleSaveClick() {
    console.log('✅ Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

حالا هر دو کامپوننت از یک منبع مشترک (useOnlineStatus) برای تشخیص وضعیت اینترنت استفاده می‌کنند. اگرچه چنین هوکی به‌صورت پیش‌فرض در React وجود ندارد، اما شما می‌توانید به‌سادگی خودتان آن را بسازید. کافی است یک تابع به نام useOnlineStatus تعریف کنید و تمام کد مربوط به مدیریت وضعیت آنلاین و آفلاین را در آن قرار دهید. useOnlineStatus.js:

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);
  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;
}

در این هوک، تمام منطق تشخیص وضعیت اینترنت در قالبی ساده و قابل‌استفاده مجدد پیاده‌سازی شده است. با این ساختار، هر زمان بخواهید وضعیت اتصال اینترنت را در هر بخش از برنامه بررسی کنید، فقط کافی است بنویسید:

const isOnline = useOnlineStatus();

اکنون کامپوننت‌های شما دیگر منطق تکراری ندارند و کد بسیار خواناتر و تمیزتر شده است. با استخراج منطق به هوک‌های سفارشی، جزئیات پیچیده‌ی ارتباط با APIها پنهان می‌شود و کد تمیزتر و قابل‌استفاده‌ی مجدد می‌گردد.

نکته مهم

نام هوک‌ها همیشه باید با use شروع شود. برنامه‌های React از کامپوننت‌ها (Components) ساخته می‌شوند، و کامپوننت‌ها خود از هوک‌ها (Hooks) تشکیل شده‌اند. معمولاً شما از هوک‌های سفارشی ساخته‌شده توسط دیگران استفاده می‌کنید، اما گاهی ممکن است لازم شود هوک مخصوص خودتان را بنویسید.
برای نام‌گذاری در React باید از چند قانون ساده پیروی کنید:

  • نام کامپوننت‌ها باید با حرف بزرگ (Capital Letter) شروع شود، مانند StatusBar یا SaveButton. همچنین کامپوننت‌ها باید چیزی بازگردانند که React بتواند آن را نمایش دهد، مثل یک بخش JSX.
  • نام هوک‌ها باید با use شروع شوند و سپس با حرف بزرگ ادامه یابند، مانند useState (هوک داخلی React) یا useOnlineStatus (هوک سفارشی که در مثال قبلی دیدیم).

رعایت این قرارداد نام‌گذاری باعث می‌شود بتوانید تنها با نگاه‌کردن به یک تابع یا کامپوننت بفهمید که کجا از state، Effect یا دیگر ویژگی‌های React استفاده شده است.

نتیجه‌گیری

در پایان می‌توان گفت که هوک‌ها بخش اصلی و مهم در React هستند. آن‌ها فقط ابزارهایی برای مدیریت state یا کنترل رفتار کامپوننت‌ها نیستند، بلکه باعث می‌شوند کدها ساده‌تر، منظم‌تر و قابل‌استفاده‌ی مجدد باشند.
هوک‌هایی مانند useOptimistic, useLayoutEffect, useInsertionEffect و useDebugValue نشان می‌دهند که React تا چه اندازه روی جزئیات تجربه‌ی کاربر و کارایی برنامه تمرکز دارد. از به‌روزرسانی آنی و خوش‌بینانه‌ی داده‌ها تا هماهنگی دقیق میان چیدمان عناصر و تزریق استایل‌ها، هرکدام از این هوک‌ها به شکلی خاص به بهبود تعامل میان کاربر و برنامه کمک می‌کنند. در کنار این‌ها، هوک‌های سفارشی (Custom Hooks) قدرت واقعی React را آشکار می‌سازند. این قابلیت باعث می‌شود توسعه‌ی پروژه‌های بزرگ سریع‌ و سازمان‌یافته‌ پیش برود.

در نهایت، یادگیری و تسلط بر این هوک‌ها تنها بخشی از مسیر است. بخش مهم‌تر آن، درک زمان و نحوه‌ی استفاده‌ی درست از آن‌هاست. یک توسعه‌دهنده‌ی حرفه‌ای React کسی است که بداند چه زمانی از هر هوک استفاده کند تا تعادل میان عملکرد، سادگی و تجربه‌ی کاربری حفظ شود. React با سیستم هوک‌های خود ثابت کرده که توسعه‌ی رابط کاربری می‌تواند هم ساده باشد و هم قدرتمند، کافی است بدانید چطور از این ابزارها هوشمندانه استفاده کنید.

reactreact.jshookهوکfront endفرانت اندری اکت

sokan-academy-footer-logo
کلیه حقوق مادی و معنوی این وب‌سایت متعلق به سکان آکادمی می باشد.