Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке hook useId у React?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**useId** - це хук React 18, який генерує стабільний унікальний ID, однаковий на сервері та клієнті. Використовується для зв'язування label з input (`htmlFor`/`id`) або ARIA атрибутів без hydration mismatch. ```tsx const id = useId(); // ":r0:" - однаково на сервері та клієнті <label htmlFor={id}>Email</label> <input id={id} /> ``` **Ключове:** на відміну від `Math.random()`, ID залишається ідентичним при SSR гідратації.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**useId** - це хук React 18, який генерує стабільний унікальний ID, що гарантовано збігається на сервері та клієнті. ## Теорія ### TL;DR - `useId` створює ID на кшталт `:r0:`, `:r1:` через внутрішній лічильник React, без random - Головна відмінність від `Math.random()`: сервер і клієнт завжди дають однакове значення - Використовуй для `htmlFor`/`id` пар та `aria-*` атрибутів у SSR-застосунках; для чисто клієнтського коду не потрібен - Ніколи не викликай всередині `.map()` - виклич один раз у компоненті, потім дописуй стабільні дані ### Швидкий приклад ```tsx import { useId } from 'react'; function EmailInput() { const id = useId(); // Той самий ":r0:" на сервері та клієнті return ( <> <label htmlFor={id}>Email</label> <input id={id} type="email" /> </> ); } ``` Один виклик, один стабільний ID. Сервер рендерить `:r0:`, браузер гідратує з `:r0:`. Жодних попереджень. ### Чому випадкові ID ламають SSR React рендерить компонент на сервері й видає HTML. Браузер отримує цей HTML і React гідратує його - прив'язує обробники подій і звіряє virtual DOM. Якщо будь-який атрибут відрізняється між серверним HTML і тим, що React генерує на клієнті, з'являється попередження про hydration mismatch. `Math.random()` дає `"0.123..."` на сервері і `"0.456..."` на клієнті. Різні значення ламають зв'язок `htmlFor`/`id`. Screen reader втрачає асоціацію між label та input. `useId` уникає цього, бо лічильник React виконує ту саму послідовність на обох сторонах. ### Як це працює React тримає глобальний лічильник на кожен корінь дерева компонентів. Під час SSR він збільшує лічильник при рендері кожного компонента й серіалізує стан. На клієнті `hydrateRoot` стартує з того самого індексу й відтворює послідовність - тому кожен виклик `useId` потрапляє на те саме число, що й на сервері. Обгортання двокрапками (`:r1:`) не випадкове. Двокрапка має спеціальне значення в CSS (псевдокласи, псевдоелементи), тому ID типу `:r0:` неможливо випадково зачепити стилями. Ці ID існують для атрибутів доступності, не для CSS. ### Коли використовувати - Пари `htmlFor`/`id` у формах з SSR - `aria-labelledby` і `aria-describedby` в доступних компонентах (тултіпи, модалки, описи) - Кілька ID в одному компоненті - один виклик `useId()`, потім суфікси: `${id}-email`, `${id}-password` Для чисто клієнтських застосунків не потрібен - там підійде будь-який унікальний рядок. Також не використовуй для `data-testid`: просто хардкодь `"email-input"` у тестах. `useId` для тестових атрибутів - зайве навантаження без жодної користі. ### Типові помилки **Виклик useId всередині .map()** ```tsx // ❌ Нові слоти при кожному рендері - ID змінюються {users.map(u => <input key={u.id} id={useId()} />)} // ✅ Виклич один раз у компоненті, дописуй стабільні дані function UserInput({ user }) { const baseId = useId(); return <input id={`${baseId}-${user.id}`} />; } ``` Кожен `useId()` всередині циклу відкриває новий слот при кожному рендері. Лічильники сервера та клієнта розходяться. **Використання для ключів списків** ```tsx // ❌ Неправильно {items.map(item => <li key={useId()}>...</li>)} // ✅ Використовуй дані з самого елемента {items.map(item => <li key={item.id}>...</li>)} ``` Ключі мають бути стабільними між рендерами. `useId` всередині map дає різні значення щоразу. **Очікування глобальної унікальності в кількох root'ах** ID обмежені одним деревом компонентів. Портали і окремі `ReactDOM.createRoot` мають власні лічильники, тому `:r1:` може з'явитись у кількох деревах. Якщо вони спільно використовують DOM - додавай префікс вручну: `useId() + '-modal'`. ### Де зустрічається у продакшені - Material-UI v6+: внутрішньо використовує `useId` для `aria-describedby` у полях вводу - React Aria (Adobe): всі зв'язки label-форм через `useId` - Chakra UI: `aria-labelledby` у модалках - Next.js App Router: серверні компоненти отримують стабільні ID для полів форм без додаткових налаштувань ### Питання на співбесіді **Q:** Чому ID має двокрапки - `:r0:`? **A:** Щоб уникнути конфліктів із CSS. ID, що починається з `:`, не можна вибрати звичайним CSS-правилом - і це навмисно. Ці ID для доступності, не для стилізації. **Q:** Що відбувається з кількома root'ами або порталами? **A:** Кожен root має свій лічильник. Якщо два root'и спільно використовують DOM, їхні ID можуть збігатись. Додавай ручний префікс для уникнення колізій. **Q:** Чи ламає React StrictMode роботу useId? **A:** Ні. StrictMode подвоює виклики ефектів у розробці, але лічильник ID зберігається для гідратації. Production SSR не зачіпається. **Q:** Чи можна задати власний формат ID? **A:** Публічного API для цього немає. Якщо потрібні читабельні ID в тестах - використовуй хардкодні `data-testid`. **Q:** (Senior) Як useId взаємодіє з Suspense boundaries у streaming SSR? **A:** Кожна Suspense boundary відслідковує лічильник ID незалежно. Коли ліниве піддерево стрімиться, воно підхоплює правильну послідовність. Mismatch виникає тільки при порушенні порядку виклику хуків всередині boundary. ## Приклади ### Форма входу з парами label-input ```tsx import { useId } from 'react'; function LoginForm() { const id = useId(); // Один базовий ID для всієї форми return ( <form> <label htmlFor={`${id}-email`}>Email</label> <input id={`${id}-email`} type="email" /> <label htmlFor={`${id}-password`}>Пароль</label> <input id={`${id}-password`} type="password" /> </form> ); } ``` Один виклик `useId` дає базу на кшталт `:r3:`. Обидва поля отримують стабільні ID (`:r3:-email`, `:r3:-password`) без другого виклику хука. На практиці такий підхід із суфіксами зручніший, ніж два окремих виклики `useId` - менше слотів, той самий результат. ### Доступний тултіп із aria-describedby ```tsx import { useId } from 'react'; function Tooltip({ text, children }: { text: string; children: React.ReactNode }) { const id = useId(); return ( <div> <div aria-describedby={id}>{children}</div> <div role="tooltip" id={id}>{text}</div> </div> ); } ``` `aria-describedby` на тригері та `id` на тултіпі мають збігатись, щоб допоміжні технології зчитували опис. На сторінці з SSR `useId` гарантує цей збіг без жодної ручної координації.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.