[React] Parsing the useId( ) hook under the microscope
Hi all!
A long time ago I spotted a relatively new hook useId
, with which I have long wanted to figure out what it is for, how it works, and of course, you definitely need to look into the source code. And now after poking this hook with a stick, reading the React documentation, scrolling through a few articles and studying a couple of YouTube videos. I am ready to share this with you. Go! (This article is a video transcript)
What is useId( ) ?
So what does a hook do? useId()
? It returns us a unique id
which looks like this: :r1:
, :r2:
, :r3:
The oddity of this format id
think and helps to be more unique on the page.
There are a lot of questions hereT:
Why do we even need such an indistinct
id
?Why
id
must be generated usingReact
? What is wrong withuuid
or with any other generatorid
?How unique
id
generated? Is it unique within the component? within the page? Or within a session?Will they stay the same
id
after page reload?
As you can see, there are a lot of questions and now from simple to complex we will answer all these questions.
Why do we need such a vague id?
To answer this question, let’s consider the following code
<label>
<span>Some label</span>
<input name="some-input" />
</label>
This is good practice when dealing with input
And label
. The main idea is that with the tag label
we wrap input
and that binds them together. What does the phrase connects mean?. If you click on the text inside the tag label
then we will see that the focus is set to input
. This is a very useful feature for users. Especially when it comes to input
type checkbox
. Click the mouse on checkbox
sometimes it can be quite difficult, but clicking on the text is much easier
For you to feel it all, I prepared a small example
But sometimes we can’t wrap the tag label
required input
and put it separately near input
. In this case, how can one connect label
And input
? For this, the tag label
there is an attribute htmlFor (JSX)
where can we send id
hung on input
. And thus establish a connection between input
And label
<div>
<label htmlFor="some-id">Some input</label>
<InfoIcon onClick={onClick} />
<input name="some-input" id="some-id" />
</div>
The only question is what value to pass in id
. We need to invent it and be sure that the current id
unique on the whole page. And how do we know to come up with a good variable name, or in our case for id
, one of the most frustrating tasks in programming. But if you think about it, we don’t need to come up with any particular meaning. It will be enough and just any awkward value
This is what the hook does. useId
. He comes up with some completely ridiculous value and guarantees its uniqueness, thereby freeing us from two tasks at once. Thinking up a name and worrying that this name is already being used by someone. Agree it’s convenient
import { useId } from 'react';
const App = () => {
const id = useId();
return (
<div>
<label htmlFor={id}>Some input</label>
<InfoIcon onClick={onClick} />
<input name="some-input" id={id} />
</div>
);
};
Complicating the Example
Well, I think we all understood the basic idea of how it works. And now let’s complicate the component a little. There are only 3 in this form. input
and it took me as many as 7 to write it id
.
const App = () => {
const formId = useId();
const firstNameId = useId();
const firstNameHintId = useId();
const lastNameId = useId();
const lastNameHintId = useId();
const emailId = useId();
const emailHintId = useId();
return (
<form id={formId} onSubmit={onSubmit}>
<label htmlFor={firstNameId}>First Name</label>
<input name="firstName" id={firstNameId} aria-describedby={firstNameHintId} />
<p id={firstNameHintId} >first name hint</p>
<label htmlFor={lastNameId}>First Name</label>
<input name="firstName" id={lastNameId} aria-describedby={lastNameHintId} />
<p id={lastNameHintId} >first name hint</p>
<label htmlFor={emailId}>First Name</label>
<input name="firstName" id={emailId} aria-describedby={emailHintId} />
<p id={emailHintId} >first name hint</p>
</form>
<button type="submit" form={formId}>Submit</button>
);
};
First I tied 3 label-а
and 3 input-а
. It’s 3 id-шки
.
After we were given a task to improve the accessibility of our form. Need to tie input-ы
with a textual description of these input-ов
. This is done with traction. aria-describedby
and surprise id-шки
HTML
theta where is the text description of this input-а
. Those. now need more id-шек
. Already collected 6
Last id
most interesting. I always use to write forms HTML
tag form
in conjunction with button type=“submit”
. But sometimes it happens that the button needs to be placed outside the tag form
. This happens especially often when the form is inside a modal window. And the buttons there are separately sewn into the default footer
. This is all fixable, of course, but often there is no time for refactoring
Fortunately, this problem has a quick solution. We also use id
we can link the form (<form id={formId}
) and button (<button type="submit" form={formId}>
) . And they will start to work well in synergy again.
As you can see in all these cases, we absolutely do not care how absurd the meanings lie in these id-шниках
(:r1:
, :r2:
, :r3:
) We just need to link these fields and we don’t really want to come up with unique names for all this. And if you continue to improve this code. Then you can create a separate generic component TextField
. Which will hide in itself the generation of all these id-шек
const TextField = ({ label, hint }) => {
const inputId = useId();
const hintId = useId();
return (
<div>
<label htmlFor={inputId}>{label}</label>
<input name="firstName" id={inputId} aria-describedby={hintId} />
<p id={hintId}>{hint}</p>
</div>
);
};
And if we switch to using this component, then no one will even really remember on the project that something is connected there id-шками
. Will just use the component TextField
. Which makes life very easy
const App = () => {
const formId = useId();
return (
<div>
<form id={formId} onSubmit={onSubmit}>
<TextField label="First Name" hint="Enter first name" />
<TextField label="Last Name" hint="Enter last name" />
<TextField label="Email" hint="Enter email" />
</form>
<button type="submit" form={formId}>Submit</button>
</div>
);
};
Array fields
And as you understand. These were not the most complex examples of forms yet. I once worked on an accounting application. Where I almost every day created some complex forms with 10 fields. In many forms, there were examples with arrays of input data
For example, in this form, we can create our own club, where it is immediately possible to add an unknown number of participants, and each participant can add the required number of hobbies. And in the end, you don’t even know how many fields the user will submit when he finishes his work. And you still need to connect everything correctly using id
and you don’t even know how many of them the user will need (demo)
Now, when creating such forms, or admins, or complex forms in a banking application, I will use the hook useId
. This is certainly not some kind of silver bullet, but it will definitely make my working days a little easier.
Why not uuid?
Now let’s answer the question why this hook should have been part of the React ecosystem. Why not just use npm
plastic bag uuid
or not generate it at all with Math.random
.
The answer is quite simple – Server Side Rendering
. As we all know, React now runs not only on the client, but also on the server. What happens if we generate id
through uuid
. It will return one value on the server. And when the first render happens on the client, we will see that id
has changed. And as a result, we will see such an error in the console
This means that the attribute id
on the server and on the client did not match. And what to do with this error is not clear, because no useState
, useMemo
or other hooks won’t help you solve this problem. I’ll have to hardcode something like this id-шники.
React has taken this problem upon itself. And provided us with a magic hook useId
. Which works in any conditions. At you Single Page Application – not a problem. Or you have Server Side Rendering – that’s not a question either, or maybe you’re trendy and use Server Components – that’s not a problem either. useId
will work in any conditions
How does it all work?
To figure out how all this magic works, you need to study the source code, of course. But before we dive into the source code, I want to remind you of one important fact about hooks. Under the hood of React, the team uses two functions instead of one hook. The first is mountId
and the second updateId
Accordingly, the first one is called when the component is first rendered, when it is just mounted. And the second method is called on all subsequent component updates. This gives more flexibility in writing code, no more need to create a bunch of if-ов
. And this logic goes beyond the hook useId
. In one of the previous videos “First Dive into Hook Sources”, I analyzed the source codes of the hooks and there you can get to know in more detail how it all works
Until then, let’s move on to mountId functions in React sources. First we get from root
some node identifierPrefix
. We will talk about it a little later, we will not linger for now.
function mountId(): string {
const hook = mountWorkInProgressHook();
const root = ((getWorkInProgressRoot(): any): FiberRoot);
const identifierPrefix = root.identifierPrefix;
// ...
}
For now, let’s move on to creating an empty variable first id
. Then comes the key if
who asks isHydration
. What is the same as asking this Server Side Rendering
? Those. the same component that is first rendered on the server, and then updated on the client
function mountId(): string {
// ...
let id;
if (getIsHydrating()) {
// ...
} else {
// ...
}
// ...
}
In case this SSR
for the base id
used treeId
, which is the same on both the server and the client. This is the whole secret of the same generation id
on the server and on the client. And it is also worth mentioning that it is unique only within the component. Those. if we have several useIds in one component, then they all have the same treeId
.
So that it wouldn’t be a problem. Below to this id
more is added localId
is just a counter from 0 to infinity. Word local
means it is local to the current component. Those. for each component, the counter starts from zero and then grows depending on how many times within one component you use the hook useId
.
function mountId(): string {
// ...
let id;
if (getIsHydrating()) {
const treeId = getTreeId();
// Use a captial R prefix for server-generated ids.
id = ':' + identifierPrefix + 'R' + treeId;
// Unless this is the first id at this level, append a number at the end
// that represents the position of this useId hook among all the useId
// hooks for this fiber.
const localId = localIdCounter++;
if (localId > 0) {
id += 'H' + localId.toString(32);
}
id += ':';
} else {
// ...
}
hook.memoizedState = id;
return id;
}
Ultimately id-шники
will look like this :R5lmcq:, :R5lmcqH1:, :R5lmcqH2:
That’s the whole generation logic id
For SSR
. If we have SPA
app, it’s still easier. There is a global counter
. The word global means that the same instance is used throughout the React application in all hooks useId
function mountId(): string {
// ...
let id;
if (getIsHydrating()) {
// ...
} else {
// Use a lowercase r prefix for client-generated ids.
const globalClientId = globalClientIdCounter++;
id = ':' + identifierPrefix + 'r' + globalClientId.toString(32) + ':';
}
hook.memoizedState = id;
return id;
}
Those. if you have only 2 for the whole project useId
in different components. Then the first will have id
equal :r1:
and the second :r2:
At the same time, an interesting fact is that let’s say you have a modal window and you use it inside useId
. Every time you mount a modal window in your application, id
will change. Those. there is no mechanism to restore the former id
. Every time you mount a component in a virtual tree, you will generate a new id
. You need to take this into account when writing code.
What is identifierPrefix?
It remains only to figure out what the previously mentioned identifierPrefix
. There is code on this topic from the documentation. The idea is simple if you have several React applications initiated in your project. In this case id-шники
may intersect. And in order to avoid this problem, they gave the opportunity to all this id-шникам
add prefix
import { createRoot } from 'react-dom/client';
import App from './App.js';
const root1 = createRoot(document.getElementById('root1'), {
identifierPrefix: 'my-first-app-'
});
root1.render(<App />);
const root2 = createRoot(document.getElementById('root2'), {
identifierPrefix: 'my-second-app-'
});
root2.render(<App />);
Examine updateId( )
updateId
much easier, all he does is take out the already saved id
and returns. After all, the whole idea is that id
will be the same throughout the lifetime of the component (Sources)
function updateId(): string {
const hook = updateWorkInProgressHook();
const id: string = hook.memoizedState;
return id;
}
Results
Let’s try to summarize what we’ve learned:
useId
– solves real problems. Yes, this is not something revolutionary, but from time to time we have to deal with this and at that moment I will be very glad that there is such a hookuseId
– not just part of the React ecosystem, because we need to be able to generate the sameid-шники
both on the server and on the client. And React took care of that.useId
– returns non-permanentid-шники
. Every time you open the modal you will get a new one.id
. If we recall the case of page reloading, then there are chances thatid-шники
remain the same, but this is nothing more than luck. I don’t know if this is good or bad. You just need to be aware of how it worksAnd of course
useId
returns uniqueid
only within a React app. If your project consists of several React applications, don’t forget to add prefixes to avoid problems
I hope after this article you will use the hook at least from time to time useId
. I’m definitely planning, although I haven’t used it yet. After all, it really solves some problems. And I love solving problems!