[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 idwhich 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 using React? What is wrong with uuid or with any other generator id?

  • 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 labelthen 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 idand 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 mountIdand 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 ifwho 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 hook

  • useId – not just part of the React ecosystem, because we need to be able to generate the same id-шники both on the server and on the client. And React took care of that.

  • useId – returns non-permanent id-шники. 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 that id-шники 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 works

  • And of course useId returns unique id 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!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *