Багатьом компонентам потрібні унікальні id для зв'язку елементів: <label for> і <input id>, aria-describedby для підказок і помилок, aria-controls для вкладок і розкривних блоків.
Наївні способи ламаються:
const id = `input-${Math.random().toString(36).slice(2)}` // різний на сервері й клієнті
let counter = 0
const id = `input-${++counter}` // залежить від порядку створення
- з SSR сервер і браузер згенерують різні значення - помилка гідрації, і
forвказуватиме на неіснуючий елемент; - глобальний лічильник на сервері продовжує рахувати між запитами різних користувачів, а в браузері починає з нуля;
- асинхронні компоненти й ліниве завантаження змінюють порядок створення компонентів.
useId() (Vue 3.5+) генерує id, стабільний між сервером і клієнтом та унікальний у межах застосунку:
<script setup lang="ts">
import { useId } from 'vue'
defineProps<{ label: string; error?: string }>()
const id = useId()
</script>
<template>
<label :for="id">{{ label }}</label>
<input :id="id" :aria-describedby="error ? `${id}-error` : undefined" />
<p v-if="error" :id="`${id}-error`">{{ error }}</p>
</template>
Один useId() на компонент - а похідні id (${id}-error, ${id}-hint) будуються суфіксами.
Правила:
- викликати в
setup(на верхньому рівні<script setup>), а не вcomputed, обробниках чи циклах - id прив'язаний до екземпляра компонента; - кілька застосунків на одній сторінці (наприклад, кілька окремих віджетів Vue на сторінці Blade) можуть згенерувати однакові id - для них задають префікс через
app.config.idPrefix; - id не варто використовувати як ключ даних чи зберігати - це лише зв'язок елементів DOM.
Навіть без SSR useId кращий за власні лічильники: передбачуваний, без глобального стану, без випадкових зіткнень.
Чому це важливо: зв'язані через id мітки й описи - основа доступності форм. Зчитувач екрана озвучує мітку поля й текст помилки, а клік на мітці фокусує поле. Зламаний id мовчки руйнує це для частини користувачів.
Докладніше в документації: Допоміжні функції Composition API: useId