Menu

Хук useId в React: уникальные id для подписей и ARIA

useId возвращает уникальный стабильный id для каждого экземпляра компонента, чтобы подпись, поле ввода и подсказка aria-describedby могли ссылаться друг на друга, даже когда компонент появляется много раз. Почему Math.random и счётчики ломаются при серверном рендеринге и почему useId не для ключей списков.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

useId возвращает уникальный id для каждого экземпляра компонента и один и тот же id при каждом рендере. Используйте его, чтобы связать <label> с его <input> или поле с подсказкой через aria-describedby, когда компонент может появиться на странице больше одного раза.

Один и тот же компонент рендерится дважды и получает два разных id, выведенных под каждым полем. Кликните по тексту «Confirm password», и фокус перейдёт на второе поле, потому что его htmlFor совпадает только с id этого поля. Захардкодьте id="password" вместо useId(), и обе подписи будут указывать на первое поле.

Синтаксис

const id = useId();

useId не принимает аргументов и возвращает строку. Вызывайте его на верхнем уровне компонента, как любой хук. Его точный формат внутренний и менялся между версиями: React 18 выдавал :r1:, а React 19.2 выдаёт _r_1_ для компонента, впервые отрендеренного в браузере, и id, начинающийся с _R_ и построенный из позиции в дереве, для отрендеренного на сервере. Никогда не разбирайте его и не полагайтесь на его форму.

Почему не Math.random и не счётчик

Id для доступности должны совпадать в HTML, который отправляет сервер, и в дереве, которое React строит в браузере. Оба очевидных способа сделать id эту проверку не проходят.

// Changes on every render, and differs between server and browser
const id = 'field-' + Math.random().toString(36).slice(2);

// The server's counter keeps growing across requests, the browser starts at 0
let nextId = 0;
const id = 'field-' + nextId++;

При серверном рендеринге (Next.js, React Router в режиме фреймворка, любая настройка с hydrateRoot) сервер печатает в HTML id="field-4817", первый рендер в браузере вычисляет field-0, и React сообщает о несоответствии гидратации. useId строит id из позиции компонента в дереве, а она одинакова с обеих сторон.

Даже без сервера id, построенный во время рендера, может меняться при каждом рендере. Этот пример показывает разницу без всякого сервера:

Нажмите кнопку несколько раз. Значение useId остаётся на месте, а id от счётчика растёт с каждым рендером, поэтому всё, что указывало на старый id (aria-describedby, подпись), теперь указывает в никуда. Если обернуть счётчик в useState(() => nextId++), проблема с повторными рендерами исчезнет, но несоответствие с сервером останется.

Несколько id из одного вызова

Компоненту с несколькими полями не нужно несколько вызовов useId. Сгенерируйте одну основу и добавляйте суффикс для каждого элемента.

Наберите в поле email слово без @: появится ошибка, а aria-describedby поля укажет на неё, поэтому экранный диктор прочитает ошибку, когда поле получит фокус. Отрендерите <SignupForm /> в App дважды, и каждая копия получит свой базовый id.

Не для ключей списков

Ключи и id решают разные задачи. Ключ говорит React, какой элемент какой между рендерами, поэтому он должен браться из данных. useId даёт один id на экземпляр компонента, и вызывать его внутри map нельзя.

// Wrong: breaks the rules of hooks, and the key is unrelated to the item
{todos.map((todo) => <Todo key={useId()} todo={todo} />)}

// Right: the key comes from the data
{todos.map((todo) => <Todo key={todo.id} todo={todo} />)}

Когда у данных нет id, создайте его при создании элемента (crypto.randomUUID() в обработчике события, который его добавляет), а не во время рендера. Страница о списках и ключах объясняет, почему ключ должен оставаться со своим элементом.

Несколько корней React на одной странице

Если на одной странице рендерятся два отдельных приложения React, их id могут совпасть. Дайте каждому корню префикс:

createRoot(document.getElementById('cart'), { identifierPrefix: 'cart-' });
createRoot(document.getElementById('chat'), { identifierPrefix: 'chat-' });

При серверном рендеринге передавайте один и тот же identifierPrefix серверному рендереру и hydrateRoot, чтобы обе стороны выдавали одинаковые id.

Частые ошибки

Разное дерево на сервере и в браузере. useId зависит от позиции компонента, поэтому ветка вроде typeof window === 'undefined' ? <A /> : <B /> над полем может сдвинуть id между двумя рендерами. Держите дерево одинаковым во время гидратации и переключайтесь после неё, в эффекте.

Поиск элемента по его id. document.getElementById(id) работает, но ref это способ React добраться до узла DOM, и ему id вообще не нужен.

Использование как случайного значения. Id уникален в пределах приложения, но не случаен и предсказуем по дереву. Не используйте его для токенов безопасности, ключей кэша или чего-либо, что хранится между сессиями.

Когда его использовать

Используйте useId всякий раз, когда переиспользуемому компоненту нужен атрибут id: поля форм из компонента, который используется много раз, подсказка, связанная через aria-describedby, диалог с aria-labelledby, вкладки с aria-controls. Когда id только связывает подпись с полем, можно обойтись без id и вложить поле в подпись (<label>Name <input /></label>); используйте useId, когда элементы нельзя вложить.

Часто задаваемые вопросы

Что делает useId в React?

Он возвращает строку, уникальную для этого экземпляра компонента и одинаковую при каждом рендере. Её используют, чтобы связывать элементы по id: htmlFor у подписи, aria-describedby у поля, aria-labelledby у диалога.

Почему не использовать Math.random() или счётчик для id?

Они дают разные значения на сервере и в браузере, поэтому страница с серверным рендерингом и её гидратированная версия расходятся, и React сообщает о несоответствии гидратации. Math.random() к тому же меняется при каждом рендере. useId выводит id из позиции компонента в дереве, а она одинакова в обоих местах.

Можно ли использовать useId для ключей в списке?

Нет. Ключ должен браться из ваших данных, чтобы React мог сопоставлять один и тот же элемент между рендерами. useId вызывается один раз на компонент, а вызов внутри map в любом случае нарушает правила хуков. Используйте собственный id элемента.

Как получить несколько id из одного вызова useId?

Вызовите useId один раз и добавляйте суффиксы: ${id}-name, ${id}-email. Основа уникальна, значит, строки с суффиксами тоже.

Можно ли использовать id из useId в селекторе CSS?

Лучше не надо. Id предназначен для связывания элементов в DOM, а его точный формат это внутренняя деталь, которая менялась между версиями React. Стилизуйте через класс, а элементы находите через ref вместо querySelector.

Иллюстрация языков программирования Coddy

Учитесь программировать с Coddy

НАЧАТЬ