Hello, everyone. I am Karsong.

See what’s wrong with the following components:

// App.tsx
const id = Math.random();

export default function App() {
  return <div id={id}>Hello</div>
}
Copy the code

If the application is CSR (Client rendering), the ID is stable and the App component is fine.

But if the application is SSR (server-side rendering), app.tsx will experience:

  1. React renders on the server and generates a random ID (let’s say 0.1234) called dehydrate

  2. Hello

    is passed as HTML to the client as the first screen content

  3. React renders on the client and generates a random ID (say 0.6789). This step is called hydrate

Client/server generated ID does not match!

In fact, there is a long-standing problem that the server and client cannot simply generate stable, unique ids. The issue was raised as early as 15 years ago:

Generating random/unique attributes server-side that don’t break client-side mounting

Until recently, Act18 released the official Hook, useId, to solve these problems. His usage is simple:

function Checkbox() {
  // Generate a unique, stable ID
  const id = useId();
  return (
    <>
      <label htmlFor={id}>Do you like React?</label>
      <input type="checkbox" name="react" id={id} />
    </>); ;Copy the code

While simple to use, the principle behind it is interesting — each ID represents the component’s hierarchy in the component tree.

In this article, let’s take a look at useId.

Welcome to join the Human high quality front-end framework research group, Band fly

Here comes Act18, and everything changes

This problem has always existed, but it has always been possible to use an incremented global count variable as id. Consider the following example:

// Global universal count variable
let globalIdIndex = 0;


export default function App() {
  const id = useState(() = > globalIdIndex++);
  return <div id={id}>Hello</div>
}
Copy the code

As long as the React process is the same on the server and client, the ids generated on both ends are the same.

However, with the advent of React Fizz (React’s new server-side streaming renderer), rendering order is no longer fixed.

For example, there is a feature called Selective Chocolate that changes the hydrate order based on user interaction.

When the left part of the screenshot is on Hydrate, the user clicks on the bottom right part:

React gives priority to the lower right part of the hydrate:

For a more detailed explanation of Selective variations, see New Suspense SSR Architecture in React 18

If the application uses the increment global count variable as the ID, then obviously the component ID will be smaller, so the ID is unstable.

So, is there any sign that both the server and the client are stable?

The answer is: the hierarchy of components.

The principle of useId

Suppose the application component tree looks like this:

No matter who comes first, B or C’s hierarchy is constant, so the hierarchy itself acts as a constant identifier between the server and the client.

For example, B can use 2-1 as its ID and C can use 2-2 as its ID:

function B() {
  / / id is "2-1"
  const id = useId();
  return <div id={id}>B</div>;
}
Copy the code

In practice, two factors need to be considered:

1. Use multiple ids for the same component

Like this:

function B() {
  const id0 = useId();
  const id1 = useId();
  return (
    <ul>
      <li id={id0}></li>
      <li id={id1}></li>
    </ul>
  );
}
Copy the code

2. Skip the components that do not use useId

Consider this component tree:

If components A and D use useId, and components B and C do not, then only A and D need to be assigned A hierarchy, thus reducing the need for presentation hierarchy.

In the actual implementation of useId, the hierarchy is represented as a number in base 32.

Base 32 is chosen because choosing the largest possible base will make the resulting string as compact as possible. Such as:

const a = 18;

// "10010" length 5
a.toString(2)   

// "i" length 1
a.toString(32)  
Copy the code

For details about the useId hierarchy, see useId

conclusion

React source code has multiple stack structures (such as a stack for storing context data).

The useId stack logic is one of the more complex ones.

Who would have thought an API so simple to use would be so complex to implement?

The React team is working on concurrency features that aren’t easy…