useId: ساخت شناسه یکتا و پایدار برای دسترس‌پذیری

از این فصل به بعد، سراغ APIهای مدرن React می‌رویم؛ Hookها و قابلیت‌هایی که هرکدام یک مشکل مشخص را حل می‌کنند. اولین مورد، useId است. این Hook مستقیماً به فصل هجدهم برمی‌گردد، جایی که دیدیم ارتباط label و input با htmlFor و id برای دسترس‌پذیری ضروری است.

مشکل: id ثابت در یک کامپوننت قابل استفاده مجدد

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

jsx
function EmailField() {
  return (
    <>
      <label htmlFor="email">ایمیل</label>
      <input id="email" />
    </>
  );
}

تا وقتی این کامپوننت فقط یک‌بار در صفحه استفاده شود، مشکلی نیست. اما همان‌طور که در فصل دوم درباره کامپوننت‌های قابل استفاده مجدد دیدیم، قرار است یک کامپوننت چند بار استفاده شود. اگر دو نمونه از EmailField در یک صفحه باشند، دو عنصر با id="email" خواهیم داشت. در HTML، id باید در کل صفحه یکتا باشد؛ در این حالت، هر دو label به اولین input اشاره می‌کنند و کلیک روی برچسب دوم، فوکوس را به فیلد اشتباه می‌برد. صفحه‌خوان هم برچسب دوم را به فیلد نادرست نسبت می‌دهد.

چرا نمی‌توان خودمان یک مقدار تصادفی ساخت

ممکن است به ذهن برسد که با Math.random() یا یک شمارنده سراسری شناسه بسازیم. این روش‌ها در React مشکل‌ساز می‌شوند. همان‌طور که در فصل یازدهم دیدیم، تابع کامپوننت باید در Render Phase خالص (Pure) باشد، و Math.random() با هر رندر مقدار متفاوتی می‌دهد. علاوه بر آن، وقتی HTML روی سرور تولید می‌شود و بعد در مرورگر دوباره با React هماهنگ می‌شود (موضوعی که در فصل بیست‌وچهارم می‌بینیم)، مقدار تصادفی سرور و کلاینت یکسان نیست و ناهماهنگی ایجاد می‌شود.

راه‌حل: useId

jsx
import { useId } from "react";

function EmailField() {
  const id = useId();

  return (
    <>
      <label htmlFor={id}>ایمیل</label>
      <input id={id} />
    </>
  );
}

useId برای هر نمونه از کامپوننت، یک رشته یکتا برمی‌گرداند. این رشته بین رندرهای مختلف همان نمونه ثابت می‌ماند، و در رندر سرور و کلاینت هم یکسان است. نتیجه این است که هر label دقیقاً به input خودش وصل می‌شود، حتی اگر ده نمونه از کامپوننت در صفحه باشد.

چند شناسه مرتبط در یک کامپوننت

اگر یک کامپوننت به چند شناسه نیاز دارد (مثلاً یکی برای input و یکی برای پیام راهنما که با aria-describedby وصل می‌شود)، لازم نیست چند بار useId را صدا بزنید. یک شناسه پایه می‌گیرید و پسوند اضافه می‌کنید:

jsx
function PasswordField() {
  const id = useId();
  const hintId = id + "-hint";

  return (
    <>
      <label htmlFor={id}>رمز عبور</label>
      <input id={id} type="password" aria-describedby={hintId} />
      <p id={hintId}>حداقل ۸ کاراکتر</p>
    </>
  );
}

چیزی که useId برای آن نیست

  • Key لیست‌ها نیست: Key باید از خود داده بیاید، مثل item.id، همان‌طور که در فصل اول و یازدهم دیدیم. اگر Key از useId یا هر مقدار تولیدشده در رندر ساخته شود، هویت آیتم‌ها بین رندرها ثابت نمی‌ماند.
  • شناسه داده نیست: برای شناسه‌ای که به پایگاه داده یا سرور ارسال می‌شود، از این Hook استفاده نکنید؛ هدف آن فقط اتصال عناصر DOM به هم است.
  • برای انتخاب‌گر CSS ایمن نیست: قالب دقیق رشته برگشتی جزو قرارداد عمومی نیست و ممکن است بین نسخه‌های React عوض شود. وابسته کردن استایل یا کد به شکل ظاهری آن درست نیست.

قانون Hookها همچنان برقرار است

useId هم مثل هر Hook دیگر، طبق Rules of Hooks فصل پنجم، باید در سطح بالای کامپوننت یا Custom Hook صدا زده شود، نه داخل شرط یا حلقه.

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

در این مثال، یک کامپوننت فیلد دو بار استفاده شده است. روی هر برچسب کلیک کنید و ببینید فوکوس به فیلد خودش می‌رود. اگر به‌جای useId یک id ثابت می‌گذاشتیم، کلیک روی برچسب دوم به فیلد اول می‌رفت.

دو نمونه از یک فیلد با شناسه یکتا
import { useId } from "react";

function TextField({ label }) {
  const id = useId();

  return (
    <div style={{ marginBottom: "12px" }}>
      <label htmlFor={id}>{label}</label>
      <br />
      <input id={id} />
    </div>
  );
}

function App() {
  return (
    <form>
      <TextField label="نام" />
      <TextField label="نام خانوادگی" />
    </form>
  );
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

useId برای هر نمونه از کامپوننت یک شناسه یکتا و پایدار می‌سازد، تا اتصال label و input و ویژگی‌هایی مثل aria-describedby حتی با چند نمونه از یک کامپوننت درست کار کند. برای چند شناسه مرتبط، یک شناسه پایه با پسوند کافی است. این Hook برای Key لیست‌ها، شناسه داده یا انتخاب‌گر CSS نیست. در درس بعدی، به useSyncExternalStore می‌پردازیم.