In Vue 3.5, why would you set app.config.idPrefix when two separate Vue apps render on the same page?
answer
- useId counters are per app
- default prefix is v
- duplicate DOM ids
- label for and aria links
- one prefix per app
basics
~20 suseId() numbers ids per app with the default prefix v, so two apps both generate v-0, v-1 and so on. Duplicate DOM ids break label and ARIA links; giving each app its own app.config.idPrefix keeps the ids unique.
solid answer
~40 sVue 3.5 added `useId()`, which returns an id that is stable for a component's position in the tree, used for things like `<label :for>` and `aria-describedby`. The counter behind it belongs to the application, and without configuration each id starts with `v`, so every app on a page produces `v-0`, `v-1` and so on. Two apps mounted side by side therefore create duplicate ids, and the browser links a label or an ARIA reference to the first matching element, which may belong to the other app. Setting `app.config.idPrefix = 'cart'` in one app and `'search'` in the other makes their ids `cart-0` and `search-0`. It must be set before mount and, for server-rendered apps, identically on server and client.
go deeper
Recall that useId() ids start at v-0 in every app and that idPrefix on app.config changes that prefix.
Explain why the counter is per app, how duplicates break label and ARIA references, and that the prefix is set before mount.
Spot the silent accessibility bug on multi-app pages and set prefixes consistently across server and client entry points.
Standardise app bootstrapping for embedded widgets so prefixes, like other per-app config, are assigned centrally rather than per team.
## Background: ids that Vue generates Accessible forms need **unique element ids**: `<label for="email">` points at `<input id="email">`, and `aria-describedby` points at the element that describes a control. Hard-coding ids breaks when a component is used twice. Vue 3.5 added `useId()`, a helper that returns a unique, render-stable id for the calling component, which also matches between server rendering and hydration. ## How the ids are built In Vue 3.5 the value is composed from three parts: 1. the app's `config.idPrefix`, or `v` when it is not set; 2. a `-` separator; 3. a counter that belongs to the **application's component tree**, plus extra segments for async boundaries. So a first app on the page produces `v-0`, `v-1`, and so on. Vue's own tests show `v-0 v-1` by default and `foo-0 foo-1` with `app.config.idPrefix = 'foo'`. ## Why two apps collide The counter restarts in every app, because every app has its own tree. A page that enhances server-rendered HTML with two independent widgets, say a cart and a search box, each created with its own `createApp`, will get: | App | idPrefix | First ids | |---|---|---| | cart | not set | `v-0`, `v-1` | | search | not set | `v-0`, `v-1` | | cart | `'cart'` | `cart-0`, `cart-1` | | search | `'search'` | `search-0`, `search-1` | With duplicates, `document.getElementById('v-0')` and every `for` or `aria-*` reference resolve to whichever element comes first in the document. The symptom is subtle: clicking the search box's label focuses an input in the cart, or a screen reader announces the wrong description. Nothing throws and no warning is printed. ## The fix - Give each app on a page a distinct `idPrefix`, set on `app.config` before `app.mount()`. - Pick prefixes that are valid at the start of an id and unlikely to collide with ids the server page already uses. - For server-rendered apps, set the same prefix on the server app and the client app, since the hydrated DOM must match the ids the server produced. ```ts import { createApp } from 'vue' import CartWidget from './CartWidget.vue' import SearchWidget from './SearchWidget.vue' const cart = createApp(CartWidget) cart.config.idPrefix = 'cart' cart.mount('#cart') const search = createApp(SearchWidget) search.config.idPrefix = 'search' search.mount('#search') ``` ## Things to know about the option - **Version.** `idPrefix` and `useId()` were both added in 3.5; on earlier versions there is nothing to configure. - **Default.** The option is `undefined` by default, and Vue falls back to `v`. - **Scope.** It affects only ids from `useId()`. Ids hard-coded in templates or generated by other libraries are unaffected. - **Format.** Treat the generated string as opaque. The documentation page shows a colon separator in its example, while the 3.5 runtime and its tests join prefix and counter with a hyphen, so code should never parse the id. ## How to catch it before users do - Automated accessibility checks flag duplicate ids, so run them on the composed page with every widget mounted, not on each widget in isolation. - A quick console check, collecting every element with an `id` and grouping by value, reveals duplicates immediately. - Clicking each label and confirming that focus lands in the same widget is the user-facing test. Because each widget passes its own tests alone, the bug only appears on integration, which is why the prefix belongs in shared bootstrap code rather than in each widget. ## When it does not matter A single app per page never collides with itself, so `idPrefix` is optional there. It becomes important with micro-frontends, several widgets on one legacy page, or a Vue app embedded in a page that already uses short ids of its own.
- Why not just generate ids with a module-level counter instead of useId()?A module counter is shared by every app and keeps counting across renders, so the server and client can produce different numbers and hydration mismatches. `useId()` ties the id to the component's position in the app's tree, so it is the same on server and client. `idPrefix` then separates apps.
- What happens if the server app and the client app use different idPrefix values?The server renders ids with one prefix and the client computes ids with another, so attributes built from `useId()` do not match during hydration. Vue reports a hydration mismatch in development, and client code that looks elements up by its own computed id will not find the server-rendered element. The fix is to share one configuration function between both entry points.
Two hotels on one street both number their rooms from 101: a note for room 101 reaches whichever hotel the courier finds first. Putting the hotel's name in front of every room number, as idPrefix does for each app, makes every address unique again.
saying these in an interview costs you the question
- useId() ids are unique across every Vue app on the page by default
- idPrefix also rewrites ids written literally in templates
- idPrefix can be set in Vue 3.4 to namespace ids
- Duplicate ids produce a Vue warning, so they are easy to notice
- Code can safely parse the counter out of a useId() value