React 18 provides useId API for generating unique IDs on both the client and server

Before diving into the useId API let us get familiar with the terms which we would be using in this blog.

Client side rendering:

In client-side rendering, the app is rendered by the client. When a browser receives a request for a page, it sends HTML, CSS and, JS code to be run in the browser. The script is loaded and the app becomes interactive. This is a way by which most of the apps are rendered.

Image is taken from a talk by Shaundai in React Conf 2021.

Server side rendering(SSR):

When the browser receives a request for a page on the server, the data is fetched for the entire application and all the react components are rendered to HTML. HTML is sent to the browser and users can see the content on the browser instead of a blank screen, thereby improving the user experience. This type of rendering is useful for heavily driven content apps.

Image is taken from a talk by Shaundai in React Conf 2021.

Refer the React Glossary to understand SSR more thoroughly.


Hydration is a process of rendering our components and attaching event handlers. It is like watering the ‘dry’ HTML with the ‘water’ of interactivity and event handlers.

Only after the hydration phase users can interact with the application. More details about hydration can be read on the glossary.

Now, take a cup of coffee or tea and let us learn about the useId and its journey!

Unique Id

To support accessibility, aria, and a11y APIs are widely used in the browser. These APIs are heavily based on IDs to link components together. aria-checked, aria-label, aria-labelledby, and aria-describedby etc, all need IDs.

Most of the time, we generate IDs wherever needed using Math.random() or any other external libraries. However, server-side rendering makes it complicated due to Id mismatch between client and server.

Different approaches were followed to fix this issue like making ids as required props on components, this._rootNodeID, simple counter , etc. But none of them were able to address all the problems faced in server side rendering -

  1. If we render on server-side and then hydrate, this could cause a hydration ID mismatch.

  2. If we render on server-side on one part of the page and render on client side on another part of the page, both the IDs could be different, which are supposed to be the same.

  3. If we conditionally render something with an ID, this might also cause a mismatch of IDs.

To fix these issues, Luna Ruan created a new hook useOpaqueIdentifier.

useOpaqueIdentifier API hook

useOpaqueIdentifier API generates a unique ID based on whether the hook was called on the server or client. If the hook is called during hydration, it generates an opaque object that will rerender the hook so that the IDs match.

Let’s checkout the example from RFC for isomorphic IDs.

As useOpaqueIdentifier is a hook, we have to follow the hook rules. So we generate the Ids at the top.

function App() {
  const tabIdOne = React.unstable_useOpaqueIdentifier();
  const panelIdOne = React.unstable_useOpaqueIdentifier();
  const tabIdTwo = React.unstable_useOpaqueIdentifier();
  const panelIdTwo = React.unstable_useOpaqueIdentifier();

  return (
      <Tabs defaultValue="one">
        <div role="tablist">
          <Tab id={tabIdOne} panelId={panelIdOne} value="one">
          <Tab id={tabIdTwo} panelId={panelIdTwo} value="one">
        <TabPanel id={panelIdOne} tabId={tabIdOne} value="one">
          Content One
        <TabPanel id={panelIdTwo} tabId={tabIdTwo} value="two">
          Content Two

Concerns in using useOpaqueIdentifier

  • To obey the hooks rules, we generate all the dynamic Ids before rendering the components. This is not a very good experience.

  • In some of the APIs like aria-labelledby, we need to concatenate the Ids. This is not supported in this API.

More work was done to fix these issues, and thereby useOpaqueIdentifier was renamed to useId.

useId API

useId API generates stable ids during server rendering and hydration to avoid mismatches. Outside of server-rendered content, it falls back to a global counter.

import React, { useId } from "react";

function App() {
  const id = useId();
  return (
      <div className="field">
        <label htmlFor={`${id}-name`} >Name</label>
        <input type="text" name="name" id={`${id}-name`} />
      <div className="field">
        <label htmlFor={`${id}-address`} >Address</label>
        <input type="text" aria-labelledBy={`${id}-name ${id}-address`} />
      <div className="field">
        <label htmlFor={`${id}-passport`} >Do you have passport?</label>
        <input type="checkbox" name="passport" id={`${id}-passport`} />

From the above example, we see that:

  • We can create globally unique dynamic ids.

  • Instead of generating separate hook N times for N different ids, we can create a single base id for the whole form, then derive further ids from that one by appending a suffix.

The dynamic Ids are base 32 strings whose binary representation corresponds to the position of a node in a tree. More details of Id generation algorithm can be found in this PR.

A brief explanation of how the Id generation is implemented can be checked out in this discussion.

Need help on your Ruby on Rails or React project?

Join Our Newsletter