Stable Accessibility IDs with React's useId Hook
Use React 18's useId hook to generate stable, hydration-safe IDs for labels and form controls without Math.random() or counters.
06 Sept 2025, 04:54 UTC

A duplicated form breaks its own labels
Imagine a settings page that renders the same address-form component twice: once for a billing address and once for a shipping address. If each input uses htmlFor="address-line-1", the browser associates every label with the first input in the document. Some teams solve this with Math.random() or a module-level counter, but those values change on every render and can differ between the server-rendered HTML and the hydrated client, triggering React hydration mismatch warnings.
The useful takeaway: React 18's useId hook generates an opaque, stable identifier tied to a component's position in the tree. It persists across re-renders and is designed to match during server-side rendering and client hydration, making it a supported choice for accessibility attributes such as id, htmlFor, aria-labelledby, and form control association.
How useId differs from ad-hoc IDs
useId is a hook introduced in React 18. Unlike Math.random(), the value does not change when the component re-renders. Unlike a counter or an array index, it does not depend on sibling order or insertion position outside the component's own subtree. React ties the value to the component tree position, so two instances of the same component receive different IDs, while a single instance keeps the same ID across updates.
The return value is intentionally opaque. Do not parse it, use it as a database key, store it in analytics, or rely on it as a deterministic selector in tests. React may change the format in a future release.
Worked example: reusable labeled input
This component pairs a label with an input using useId. You can render it multiple times in one tree without ID collisions.
import { useId } from 'react';
function LabeledInput({ labelText }) {
const id = useId();
return (
{labelText}
);
}
export default function AddressForm() {
return (
);
}
If you render AddressForm twice on the same page, each generated id and matching htmlFor pair will differ, so labels remain bound to their own inputs. Re-rendering the form after a state change keeps each ID stable.
Verify it before you ship
Confirm your project depends on React and React DOM at 18.0.0 or later. Run this in your project root; it requires read access to package.json.
# Check installed React versions
node -e "const p=require('./package.json'); console.log(p.dependencies.react, p.dependencies['react-dom'])"
Then perform these checks:
- Render a component that uses
useIdon the server and hydrate it on the client. Open the browser console and confirm there is no React hydration mismatch warning related to the generated ID. - Render the same component twice in one tree. Inspect the DOM and confirm the two
id/htmlForpairs are different. - Trigger a re-render of one instance, for example by typing into an unrelated input in the same form. Confirm the ID for that instance does not change.
If you are upgrading from React 17, plan for regression testing on hydration behavior and screen-reader announcements, because useId is unavailable in earlier versions.
Limitations
useIdrequires React 18 or later.- The ID is scoped to one React tree. It is not designed to coordinate identifiers between separate React roots or non-React markup on the same page.
- It supplies identifiers only. It does not manage focus, validation, keyboard behavior, or error messages.
- Do not use the value as a business key, analytics identifier, or stable test selector.
Rollback
Because adding useId changes component code, rollback means reverting the component file to its previous ID strategy, such as a prop-passed ID or a server-generated identifier.
Practical check after adding useId
Open your rendered page in browser DevTools, select one of the generated inputs, and confirm that its id exactly matches the htmlFor of its associated label. Repeat for the second instance. If the values match and persist across interactions, the hook is behaving as intended.
Use it for IDs, not for keys
useId is the right tool when you need a stable, HTML-safe identifier for accessibility. It is not a replacement for React list key props, database identifiers, or cross-page tracking. If your component already receives a unique ID from a parent or backend, prefer that prop over generating one internally.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.