مقالات › React

آموزش کامل هوک‌های React؛ useState، useEffect، useRef، useMemo و بیشتر

معرفی کاربردی هوک‌های اصلی React: هر هوک چطور کار می‌کند، کِی باید از آن استفاده کرد و چه قوانین و اشتباهاتی را هر برنامه‌نویس React باید بداند.

هوک‌ها React را متحول کردند. از نسخه‌ی 16.8 به بعد، کامپوننت‌های تابعی می‌توانند State داشته باشند، Side Effect اجرا کنند و به Context دسترسی پیدا کنند؛ کارهایی که قبلاً فقط با کلاس ممکن بود. امروز هوک‌ها خودِ React هستند. در این آموزش هوک‌هایی را که هر روز استفاده می‌کنید با مثال‌های کاربردی و مدل ذهنی پشت هرکدام مرور می‌کنیم.

دو قانون طلایی هوک‌ها

قبل از هر چیز، قوانینی را که React به آن‌ها وابسته است به خاطر بسپارید:

  1. هوک‌ها را فقط در بالاترین سطح کامپوننت یا هوک سفارشی صدا بزنید؛ هرگز داخل شرط، حلقه یا تابع تودرتو.
  2. هوک‌ها را فقط از توابع React صدا بزنید؛ یعنی کامپوننت‌ها و هوک‌های سفارشی، نه توابع کمکی معمولی.

React هر هوک را با ترتیب فراخوانی آن می‌شناسد. اگر در یک رندر یک هوک جا بیفتد، همه‌ی هوک‌های بعدی State اشتباه دریافت می‌کنند. پکیج eslint-plugin-react-hooks این قوانین را خودکار بررسی می‌کند؛ حتماً نصبش کنید.

useState؛ وضعیت محلی

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount((c) => c + 1)}>
      Clicked {count} times
    </button>
  );
}

نکته‌های کلیدی:

  • صدا زدن Setter یک رندر جدید را زمان‌بندی می‌کند؛ مقدار count در همین رندر عوض نمی‌شود.
  • وقتی State بعدی به مقدار قبلی وابسته است، از تابع به‌روزرسان setCount(c => c + 1) استفاده کنید. در این صورت چند به‌روزرسانی در یک رویداد به‌درستی اعمال می‌شوند.
  • State جایگزین می‌شود، نه ادغام. برای اشیا مقدار قبلی را کپی کنید:
setForm((prev) => ({ ...prev, email: value }));
  • برای مقدار اولیه‌ی سنگین یک تابع پاس بدهید: useState(() => loadFromStorage()). این تابع فقط در رندر اول اجرا می‌شود.

useEffect؛ هماهنگی با دنیای بیرون

useEffect بعد از به‌روزرسانی DOM اجرا می‌شود. از آن برای هماهنگ کردن کامپوننت با چیزی بیرون از React استفاده کنید: درخواست شبکه، Subscription، تایمر، عنوان صفحه یا ویجت‌های خارجی.

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    let cancelled = false;

    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => {
        if (!cancelled) setUser(data);
      });

    return () => {
      cancelled = true; // Cleanup: پاسخ‌های قدیمی نادیده گرفته شوند
    };
  }, [userId]);

  return user ? <h2>{user.name}</h2> : <p>Loading…</p>;
}

آرایه‌ی وابستگی‌ها تعیین می‌کند Effect کِی دوباره اجرا شود:

وابستگی‌ها زمان اجرا
بدون آرایه بعد از هر رندر
[] یک بار بعد از Mount (و Cleanup هنگام Unmount)
[a, b] بعد از Mount و هر بار که a یا b تغییر کند

تابعی که از Effect برمی‌گردد Cleanup است. React آن را قبل از اجرای مجدد Effect و هنگام Unmount شدن کامپوننت صدا می‌زند؛ مناسب برای حذف Event Listener، پاک کردن تایمر یا لغو درخواست.

نکته: اگر چیزی را می‌توان در زمان رندر از Props یا State محاسبه کرد، جایش در Effect نیست. مستقیم محاسبه‌اش کنید. Effect یک راه فرار است، نه ابزار جریان داده.

useRef؛ مقداری که بدون رندر مجدد باقی می‌ماند

useRef یک شیء تغییرپذیر { current } برمی‌گرداند که بین رندرها حفظ می‌شود. تغییر آن رندر جدید ایجاد نمی‌کند. دو کاربرد رایج:

// ۱) دسترسی به یک المان DOM
function SearchBox() {
  const inputRef = useRef(null);
  useEffect(() => inputRef.current.focus(), []);
  return <input ref={inputRef} />;
}

// ۲) نگه‌داشتن یک مقدار تغییرپذیر (شناسه‌ی تایمر، مقدار قبلی، نمونه‌ی یک کلاس)
function Stopwatch() {
  const intervalRef = useRef(null);
  const start = () => {
    intervalRef.current = setInterval(() => console.log("tick"), 1000);
  };
  const stop = () => clearInterval(intervalRef.current);
  return (
    <>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </>
  );
}

قاعده‌ی سرانگشتی: اگر مقدار روی صفحه نمایش داده می‌شود، State؛ اگر فقط برای بعد به خاطر سپرده می‌شود، Ref.

useMemo و useCallback؛ کش کردن بین رندرها

useMemo نتیجه‌ی یک محاسبه را کش می‌کند و useCallback یک تابع را.

function ProductList({ products, query }) {
  const filtered = useMemo(
    () => products.filter((p) => p.name.toLowerCase().includes(query.toLowerCase())),
    [products, query]
  );

  const handleSelect = useCallback((id) => {
    console.log("selected", id);
  }, []);

  return filtered.map((p) => <ProductRow key={p.id} product={p} onSelect={handleSelect} />);
}

این دو در دو موقعیت اهمیت دارند:

  1. محاسبه واقعاً سنگین است (فیلتر هزاران آیتم، قالب‌بندی‌های پیچیده).
  2. اشیا یا توابع را به فرزندان Memo‌شده (React.memo) یا آرایه‌ی وابستگی هوک دیگری می‌دهید و به ارجاع (Reference) ثابت نیاز دارید.

همه‌چیز را پیش‌فرض داخل این هوک‌ها نپیچید؛ Memoization هم هزینه دارد. اول با Profiler در React DevTools اندازه بگیرید. (React Compiler حالا می‌تواند بسیاری از این بهینه‌سازی‌ها را خودکار انجام دهد.)

useContext؛ خداحافظی با Prop Drilling

const ThemeContext = createContext("light");

function App() {
  const [theme, setTheme] = useState("dark");
  return (
    <ThemeContext.Provider value={theme}>
      <Toolbar onToggle={() => setTheme((t) => (t === "dark" ? "light" : "dark"))} />
    </ThemeContext.Provider>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button className={`btn btn--${theme}`}>Save</button>;
}

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

useReducer؛ تغییرات پیچیده‌ی State

وقتی چند بخش از State با هم تغییر می‌کنند، Reducer منطق را در یک جای قابل پیش‌بینی نگه می‌دارد:

function reducer(state, action) {
  switch (action.type) {
    case "added":
      return { ...state, items: [...state.items, action.item] };
    case "removed":
      return { ...state, items: state.items.filter((i) => i.id !== action.id) };
    case "cleared":
      return { ...state, items: [] };
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

function Cart() {
  const [state, dispatch] = useReducer(reducer, { items: [] });
  // dispatch({ type: "added", item })
}

Reducerها توابع خالص (Pure) هستند و بدون رندر کردن چیزی به‌راحتی تست می‌شوند.

هوک‌های دیگری که ارزش شناختن دارند

  • useId — شناسه‌ی یکتای پایدار برای ویژگی‌های دسترس‌پذیری (htmlFor، aria-describedby) می‌سازد.
  • useLayoutEffect — مثل useEffect است اما قبل از نقاشی صفحه توسط مرورگر اجرا می‌شود. برای اندازه‌گیری DOM و جلوگیری از پرش (انیمیشن، Tooltip) مناسب است.
  • useTransition — یک به‌روزرسانی را غیرفوری علامت می‌زند تا تایپ کردن روان بماند، حتی وقتی یک لیست سنگین رندر می‌شود.
  • useDeferredValue — نسخه‌ی «با تأخیر» یک مقدار را برای رابط‌های سنگین رندر می‌کند.
  • useSyncExternalStore — اشتراک امن در Storeهای بیرونی (APIهای مرورگر، کتابخانه‌های State).

اشتباهات رایج

  1. فراموش کردن وابستگی‌ها — خواندن یک Prop داخل Effect بدون آوردن آن در آرایه، مقدار کهنه (Stale) تولید می‌کند. به Linter اعتماد کنید.
  2. حلقه‌ی بی‌نهایت — Set کردن State داخل Effectی که به همان State وابسته است.
  3. شیء در آرایه‌ی وابستگی — {} که هنگام رندر ساخته می‌شود هر بار یک شیء جدید است و Effect در هر رندر اجرا می‌شود.
  4. Fetch بدون Cleanup — با جابه‌جایی سریع بین صفحات ممکن است پاسخ یک درخواست قدیمی نمایش داده شود.
  5. کپی کردن Props در State — معمولاً لازم نیست؛ مقدار را مستقیم محاسبه کنید.

جمع‌بندی

useState چیزی را که در طول زمان تغییر می‌کند نگه می‌دارد، useEffect با دنیای بیرون هماهنگ می‌کند، useRef مقادیر را بدون رندر مجدد به خاطر می‌سپارد، useMemo و useCallback کار و ارجاع‌ها را کش می‌کنند، useContext داده را در درخت کامپوننت‌ها به اشتراک می‌گذارد و useReducer تغییرات پیچیده را سازمان‌دهی می‌کند. قوانین هوک‌ها را رعایت کنید، Effectها را حداقلی نگه دارید و هرچه می‌توانید را مستقیم محاسبه کنید؛ این هسته‌ی React مدرن است.

پرسش‌های متداول

چرا Effect من در حالت توسعه دو بار اجرا می‌شود؟

در StrictMode، React در محیط توسعه کامپوننت‌ها را Mount، Unmount و دوباره Mount می‌کند تا Cleanupهای جاافتاده را پیدا کنید. این اتفاق در Production نمی‌افتد.

برای گرفتن داده از useEffect استفاده کنم؟

کار می‌کند، اما کتابخانه‌هایی مثل TanStack Query کش، حذف درخواست‌های تکراری، تلاش مجدد و Race Condition را برای شما مدیریت می‌کنند. در پروژه‌های واقعی معمولاً گزینه‌ی بهتری هستند.

کِی باید هوک سفارشی بسازم؟

وقتی دو یا چند کامپوننت منطق Stateدار مشترکی دارند: دریافت داده، مدیریت فرم، Media Query یا Local Storage. هوک سفارشی «منطق» را به اشتراک می‌گذارد، نه خود State را.