This content originally appeared on DEV Community and was authored by Ali Aslam
useId
might seem like one of those “tiny utility” hooks you’ll barely use — until you realize it quietly solves problems you didn’t even know you had.
Before we dive in, here’s your roadmap so you can jump straight to the parts you’re most curious about.
Table of Contents
Why useId
Exists
- The “fix” people try
- Accessibility makes this even more important
- Enter
useId
How useId
Works
- Stable across renders
- Consistent between server and client
- Auto-unique in the React tree
- Plays well with multiple instances
The API
- Syntax
- Basic usage
- Why it works so well here
- IDs are strings, so you can modify them
Basic Example
- What’s happening
- Why it’s better than hardcoding
Advanced Patterns
- Prefixing for readability
- Multiple IDs in one component
- Fallback IDs
- Not for list keys
Pitfalls & What useId
Is Not
- Not for list keys
- Not random
- Not globally unique outside React
- Not reactive
- Won’t fix bad accessibility by itself
- Testing considerations
Wrap-up
- The mental checklist
Why useId
Exists
Let’s play a game:
You’re building a form. It’s a simple “name + email” situation, so you do this:
<label htmlFor="name">Name</label>
<input id="name" />
<label htmlFor="email">Email</label>
<input id="email" />
Works fine, right?
Until… you render two copies of this form on the same page (maybe one is in a modal, one is in the main content).
Now you have:
- Two inputs with
id="name"
. - Two inputs with
id="email"
.
In HTML, IDs are supposed to be unique. Duplicate IDs = broken accessibility and sometimes weird browser behavior.
The “fix” people try
Some devs reach for Math.random()
or Date.now()
to generate IDs:
const id = Math.random();
Or they make a global counter and increment it for each new component instance.
This works most of the time in client-only React…
…but it breaks horribly in server-side rendering (SSR) and hydration.
Why?
Because the server renders one set of random IDs, and the client renders another set when hydrating.
React sees the mismatch and either complains loudly (hydration warnings) or re-renders the whole subtree.
Accessibility makes this even more important
IDs aren’t just for CSS — they’re critical for accessibility:
- Connecting a
<label>
to its<input>
viahtmlFor
. - Linking error messages to inputs with
aria-describedby
. - Creating relationships between elements with
aria-labelledby
.
If the IDs are inconsistent between renders, those accessibility relationships break.
And users relying on screen readers pay the price.
Enter useId
React needed a way to:
- Generate unique IDs per component instance.
- Keep them stable across renders.
- Make them match between server and client for SSR.
That’s exactly what useId
does — no counters, no random numbers, no mismatches.
Just safe, predictable, hydration-friendly IDs.
How useId
Works
useId
feels like magic when you first try it — you call a hook, it hands you a unique string, and you’re done.
But there’s a method to the magic, and understanding it makes it easier to use correctly.
1. Stable across renders
Once a component calls useId()
, the ID it returns never changes for that instance.
Even if the component re-renders 50 times because of state updates, you get the exact same string back every time.
function Example() {
const id = React.useId();
const [count, setCount] = React.useState(0);
console.log(id); // Same every render
return <button onClick={() => setCount(c => c + 1)}>Click me</button>;
}
Why it matters:
- Your
label
stays connected to yourinput
without flipping IDs mid-session. - No “flashing” or mismatching accessibility relationships.
2. Consistent between server and client
This is the big win over random IDs.
useId
is designed for SSR: React generates the same ID string on the server and the client during hydration.
No mismatches → no React warnings → no broken UI.
If you’ve ever seen this warning:
Warning: Prop `id` did not match. Server: ":r0:" Client: ":r1:"
useId
is your cure.
This server–client consistency is especially important in React 18+ concurrent rendering and streaming SSR.
In these modes, parts of your UI may render at different times on the server, but useId ensures the final HTML still has a predictable ID sequence that matches the client.
Without this, hydration would break because the client’s IDs wouldn’t line up with the server’s streamed HTML.
3. Auto-unique in the React tree
Each call to useId()
gets its own unique string within the current render tree.
Even if you have 50 inputs across different components, they’ll all get different IDs without you lifting a finger.
Example output:
:r0:
:r1:
:r2:
You don’t need to know or care about the exact format — React just guarantees they won’t clash.
4. Plays well with multiple instances
If you render the same component twice:
<MyForm />
<MyForm />
Each <MyForm>
gets its own set of IDs.
They’re unique between instances, but stable inside each one.
The API
If you’ve been bracing yourself for something complicated… you can relax.
useId
has one of the simplest APIs in all of React.
Syntax
const id = useId();
- No arguments — you don’t pass anything in.
- Returns — a unique, stable string.
Basic usage
Let’s wire it up in a form:
import { useId } from "react";
function NameField() {
const id = useId();
return (
<div>
<label htmlFor={id}>Name</label>
<input id={id} type="text" />
</div>
);
}
That’s it.
Now, if you render <NameField />
multiple times, each one gets a different ID, but it’s always the same inside that component.
Why it works so well here
-
label
→htmlFor
→ matches theinput
’sid
. - Works the same in client-only and SSR apps.
- No manual ID naming or risk of collisions.
IDs are strings, so you can modify them
You’re not stuck with the raw :r0:
format.
Want something more descriptive? Prefix it.
const id = useId();
<input id={`user-${id}`} />
React still guarantees uniqueness — your prefix just makes it easier to debug.
Basic Example
Let’s build a small but realistic form that needs multiple unique IDs — one for each label/input pair.
import { useId } from "react";
function SignupForm() {
const nameId = useId();
const emailId = useId();
return (
<form>
<div>
<label htmlFor={nameId}>Name</label>
<input id={nameId} type="text" />
</div>
<div>
<label htmlFor={emailId}>Email</label>
<input id={emailId} type="email" />
</div>
<button type="submit">Sign Up</button>
</form>
);
}
What’s happening
- Each call to
useId()
gives a unique string. - The first render assigns it, and it never changes for that instance.
- Even if you render
<SignupForm />
twice, each instance has its own IDs — no collisions.
Why it’s better than hardcoding
If you did:
<label htmlFor="email">Email</label>
<input id="email" />
…and rendered two forms, you’d have two elements with id="email"
on the page — a no-no for accessibility and potentially confusing for the browser.
useId
makes sure every instance stays unique and safe.
Advanced Patterns
Once you’ve got the basics down, useId
can do a bit more than just the plain id={id}
pattern.
Here are some useful tricks you’ll probably run into.
1. Prefixing for readability
The default IDs look like :r0:
, which is fine for React, but not very human-friendly when you’re debugging.
You can add a prefix:
const id = useId();
<input id={`user-${id}`} />
Output might look like:
id="user-:r2:"
It’s still unique, but now you instantly know which element it’s tied to.
2. Multiple IDs in one component
Need more than one unique ID? Just call useId()
multiple times.
function ProfileForm() {
const usernameId = useId();
const bioId = useId();
return (
<>
<label htmlFor={usernameId}>Username</label>
<input id={usernameId} />
<label htmlFor={bioId}>Bio</label>
<textarea id={bioId} />
</>
);
}
Each call is stable and unique.
3. Fallback IDs
Sometimes you want the parent component to decide the ID, but still have a fallback if none is provided.
function InputWithOptionalId({ id: customId, label }) {
const generatedId = useId();
const id = customId || generatedId;
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} />
</>
);
}
Now your component is flexible and safe.
Advanced note:
useId also plays nicely with portals. Even if you render part of your UI into a completely different part of the DOM (e.g., a modal root), useId still guarantees uniqueness across the entire React tree. This works because the ID generation is tied to the React fiber tree, not the DOM position.
4. Not for list keys
This is important — useId
is not a replacement for stable list keys when rendering arrays.
Why?
Because keys need to come from your data, not from a hook that’s called each render.
Bad:
items.map(item => <li key={useId()}>{item.name}</li>)
Good:
items.map(item => <li key={item.id}>{item.name}</li>)
useId
IDs are stable within a component instance, but they’ll change if the component is unmounted/remounted — which would break React’s list diffing.
Pitfalls & What useId
Is Not
useId
is great — but only if you use it for what it was designed to do.
Here are the most common misunderstandings (and how to avoid them).
1. Not for list keys
We already touched on this, but it’s worth repeating because it’s the #1 misuse.
React keys need to come from your data — something that persists even if a component unmounts and remounts.
useId
IDs reset when the component is recreated, so they can’t guarantee stable keys for lists.
2. Not random
If you’re expecting useId
to give you unpredictable IDs like a UUID, nope.
It’s deterministic — React uses an internal counter so it can match IDs between server and client.
That means you shouldn’t rely on it for things like generating unique database IDs or session tokens.
3. Not globally unique outside React
useId
guarantees uniqueness within the React render tree, not across multiple apps on the same page.
If you embed two separate React apps in the same DOM, their useId()
outputs could overlap.
4. Not reactive
useId
is stable — it doesn’t change when state changes.
If you need an ID that updates when something else changes, useId
isn’t the right tool (and honestly, you almost never want that for DOM IDs).
5. Won’t fix bad accessibility by itself
useId
helps connect elements reliably, but it’s still up to you to use the right ARIA attributes and semantic HTML.
If you misuse aria-labelledby
or htmlFor
, useId
won’t magically fix it.
Testing considerations
If you rely on getById or similar queries in tests, remember that useId output changes between test runs.
Instead of hardcoding the exact ID string, query by label text, role, or data-testid to avoid brittle tests.
Example with Testing Library:
const input = screen.getByLabelText(/email/i);
This way your tests remain stable even if the generated IDs change.
Wrap-up
useId
might not be the flashiest hook in React’s toolbox, but it quietly solves a surprisingly tricky problem:
generating unique, stable, SSR-friendly IDs without you having to think about it.
The mental checklist
Reach for useId
when:
- You need to connect a
<label>
with an<input>
reliably. - You’re wiring up ARIA attributes like
aria-labelledby
oraria-describedby
. - You want an SSR-safe way to generate unique IDs that stay stable across renders.
Avoid it when:
- You’re creating list keys → use stable data IDs instead.
- You need a random or unpredictable identifier (use something like
crypto.randomUUID()
for that). - You’re working outside the React render tree (e.g., generating IDs in a non-React script).
Think of it this way:
If the ID’s main job is to link two DOM elements together within your React app, useId
is perfect.
If you’re trying to track data, users, or sessions, you need a different tool.
Next in our series, Next, let’s tackle how React 19 makes handling forms easier and more powerful with React 19 Deep Dive — Forms & Actions with useFormState
, useFormStatus
, and useOptimistic
.
Follow me on DEV for future posts in this deep-dive series.
https://dev.to/a1guy
If it helped, leave a reaction (heart / bookmark) — it keeps me motivated to create more content
Want video demos? Subscribe on YouTube: @LearnAwesome
This content originally appeared on DEV Community and was authored by Ali Aslam