useImperativeHandle در جایگاه APIهای مدرن

این Hook را قبلاً در فصل هفتم، درس ۷-۶، دیدیم. آنجا تمرکز روی ایده اصلی بود: به‌جای بیرون دادن کل عنصر DOM از طریق یک Ref، فقط چند تابع مشخص را در اختیار بیرون بگذاریم. چون این Hook در فهرست APIهای مدرن React هم جا دارد، در این درس تکرار آن مطلب را کنار می‌گذاریم و سراغ سه چیز می‌رویم که آنجا نیامد: جایگاه آن بعد از «ref به‌عنوان Prop»، آرگومان سوم آن، و معیار تصمیم‌گیری برای استفاده یا پرهیز از آن.

یادآوری کوتاه

jsx
useImperativeHandle(ref, () => ({
  focus() {
    inputRef.current.focus();
  },
}));

تابع دوم، آبجکتی می‌سازد که مقدار ref.current در کامپوننت والد خواهد بود. والد فقط همین متدها را می‌بیند، نه خود عنصر DOM را.

جایگاه آن بعد از ref به‌عنوان Prop

در درس ۷-۵ دیدیم که در React مدرن (از نسخه ۱۹)، یک Function Component می‌تواند ref را مثل هر Prop دیگری در پارامترهایش بگیرد. پیش از آن، برای این کار باید کامپوننت با forwardRef پیچیده می‌شد، که امروز روش Legacy محسوب می‌شود. این تغییر باعث شد نحو useImperativeHandle هم ساده‌تر شود:

jsx
// روش قدیمی (Legacy)
const Field = forwardRef((props, ref) => {
  useImperativeHandle(ref, () => ({ /* ... */ }));
  return <input />;
});

// روش مدرن
function Field({ ref }) {
  useImperativeHandle(ref, () => ({ /* ... */ }));
  return <input />;
}

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

آرگومان سوم: Dependency Array

useImperativeHandle یک آرگومان سوم اختیاری هم دارد که شبیه Dependency Array در useEffect و useMemo کار می‌کند:

jsx
useImperativeHandle(ref, () => ({
  reset() {
    setValue(initialValue);
  },
}), [initialValue]);

بدون این آرایه، آبجکت Handle با هر رندر دوباره ساخته می‌شود. با آرایه، فقط وقتی یکی از مقادیر آن تغییر کند دوباره ساخته می‌شود. دلیل اهمیت آن همان قاعده‌ای است که در فصل سیزدهم دیدیم: هر مقداری که تابع از بیرون می‌خواند باید در آرایه باشد، وگرنه متدهای Handle به مقدار قدیمی همان رندر اشاره می‌کنند (Stale Closure، فصل ششم).

معیار تصمیم‌گیری: اول Prop، بعد Handle

این Hook ما را به سبک دستوری (Imperative) می‌برد: «همین الان این کار را انجام بده». اما مدل اصلی React توصیفی است. پیش از رفتن سراغ Handle، بهتر است بپرسید آیا همین نیاز را می‌توان با Prop یا State حل کرد؟ مثلاً به‌جای متد open() و close() برای یک Modal، یک Prop به نام isOpen معمولاً خواناتر و قابل‌پیش‌بینی‌تر است. موارد منطقی برای Handle، کارهایی هستند که ذاتاً رویدادی و یک‌باره‌اند و با State خوب بیان نمی‌شوند:

  • فوکوس دادن به یک فیلد
  • اسکرول به یک موقعیت مشخص
  • شروع یا توقف پخش یک رسانه

Handle را کوچک و روشن نگه دارید

همان‌طور که در درس طراحی API برای Custom Hook (فصل دهم) دیدیم، هر چیزی که بیرون بدهید بخشی از رابط عمومی می‌شود. هرچه متدهای Handle کمتر و نام‌هایشان گویاتر باشد، تغییر پیاده‌سازی داخلی کامپوننت راحت‌تر است و والد کمتر به جزئیات آن وابسته می‌شود.

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

در این مثال، کامپوننت لیست فقط دو متد scrollToTop و scrollToBottom را بیرون می‌دهد. والد به خود عنصر DOM لیست دسترسی ندارد. دکمه‌ها را بزنید و رفتار لیست را ببینید.

Handle محدود برای کنترل اسکرول یک لیست
import { useRef, useImperativeHandle } from "react";

function ScrollList({ ref, items }) {
  const listRef = useRef(null);

  useImperativeHandle(ref, () => ({
    scrollToTop() {
      listRef.current.scrollTo({ top: 0, behavior: "smooth" });
    },
    scrollToBottom() {
      const el = listRef.current;
      el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
    },
  }), []);

  return (
    <ul
      ref={listRef}
      style={{ height: "120px", overflowY: "auto", border: "1px solid gray", padding: "8px" }}
    >
      {items.map((item) => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
}

function App() {
  const listHandle = useRef(null);
  const items = Array.from({ length: 30 }, (_, i) => "آیتم " + (i + 1));

  return (
    <div>
      <button onClick={() => listHandle.current.scrollToTop()}>رفتن به بالا</button>
      <button onClick={() => listHandle.current.scrollToBottom()}>رفتن به پایین</button>
      <ScrollList ref={listHandle} items={items} />
    </div>
  );
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

useImperativeHandle با ref به‌عنوان Prop نحو ساده‌تری پیدا کرده و forwardRef دیگر روش توصیه‌شده برای کد جدید نیست. آرگومان سوم آن، مثل Hookهای دیگر، تعیین می‌کند Handle چه زمانی دوباره ساخته شود و باید همه مقادیر خوانده‌شده را در بر بگیرد. این Hook برای کارهای رویدادی و یک‌باره مثل فوکوس و اسکرول مناسب است، و برای هر چیزی که با Prop یا State می‌شود توصیف کرد، روش توصیفی ترجیح دارد. در درس بعدی، به Actions می‌پردازیم.