useId 是一个 React Hook,用于生成可以传递给无障碍属性的唯一 ID。

const id = useId()

参考

🌐 Reference

useId()

在组件的顶层调用 useId 以生成一个唯一的 ID:

🌐 Call useId at the top level of your component to generate a unique ID:

import { useId } from 'react';

function PasswordField() {
const passwordHintId = useId();
// ...

查看更多示例。

参数

🌐 Parameters

useId 不接受任何参数。

返回

🌐 Returns

useId 返回与此特定组件中的特定 useId 调用关联的唯一 ID 字符串。

注意事项

🌐 Caveats

  • useId 是一个 Hook,因此你只能在你的组件的顶层或你自己的 Hook 中调用它。你不能在循环或条件语句中调用它。如果你需要那样做,可以提取一个新组件并将状态移入其中。
  • useId 不应用于为 use() 生成缓存键。组件挂载时 ID 是稳定的,但在渲染过程中可能会改变。缓存键应根据你的数据生成。
  • useId 不应在列表中用作生成键键应从你的数据生成。
  • useId 目前无法在 异步服务器组件 中使用。

用法

🌐 Usage

易犯错误

不要在列表中调用 useId 来生成密钥。 密钥应从你的数据中生成。

为可访问性属性生成唯一 ID

🌐 Generating unique IDs for accessibility attributes

在组件的顶层调用 useId 以生成一个唯一的 ID:

🌐 Call useId at the top level of your component to generate a unique ID:

import { useId } from 'react';

function PasswordField() {
const passwordHintId = useId();
// ...

然后你可以将 生成的ID 传递给不同的属性:

<>
<input type="password" aria-describedby={passwordHintId} />
<p id={passwordHintId}>
</>

让我们通过一个例子来看看这什么时候有用。

HTML 可访问性属性aria-describedby 让你可以指定两个标签彼此关联。例如,你可以指定一个元素(如输入框)由另一个元素(如段落)描述。

在常规 HTML 中,你可以这样写:

🌐 In regular HTML, you would write it like this:

<label>
Password:
<input
type="password"
aria-describedby="password-hint"
/>
</label>
<p id="password-hint">
The password should contain at least 18 characters
</p>

然而,在 React 中像这样硬编码 ID 并不是一个好做法。一个组件可能在页面上被渲染多次——但 ID 必须是唯一的!与其硬编码 ID,不如使用 useId 生成一个唯一的 ID:

🌐 However, hardcoding IDs like this is not a good practice in React. A component may be rendered more than once on the page—but IDs have to be unique! Instead of hardcoding an ID, generate a unique ID with useId:

import { useId } from 'react';

function PasswordField() {
const passwordHintId = useId();
return (
<>
<label>
Password:
<input
type="password"
aria-describedby={passwordHintId}
/>
</label>
<p id={passwordHintId}>
The password should contain at least 18 characters
</p>
</>
);
}

现在,即使 PasswordField 在屏幕上出现多次,生成的 ID 也不会冲突。

🌐 Now, even if PasswordField appears multiple times on the screen, the generated IDs won’t clash.

import { useId } from 'react';

function PasswordField() {
  const passwordHintId = useId();
  return (
    <>
      <label>
        Password:
        <input
          type="password"
          aria-describedby={passwordHintId}
        />
      </label>
      <p id={passwordHintId}>
        The password should contain at least 18 characters
      </p>
    </>
  );
}

export default function App() {
  return (
    <>
      <h2>Choose password</h2>
      <PasswordField />
      <h2>Confirm password</h2>
      <PasswordField />
    </>
  );
}

观看此视频 以了解使用辅助技术时的用户体验差异。

易犯错误

使用 服务器渲染 时,useId 需要服务器和客户端上的组件树完全相同。如果你在服务器和客户端渲染的树不完全匹配,则生成的 ID 将不匹配。

🌐 With server rendering, useId requires an identical component tree on the server and the client. If the trees you render on the server and the client don’t match exactly, the generated IDs won’t match.

深入研究

为什么 useId 比递增计数器更好?

🌐 Why is useId better than an incrementing counter?

你可能想知道为什么 useId 比增加像 nextId++ 这样的全局变量更好。

🌐 You might be wondering why useId is better than incrementing a global variable like nextId++.

useId的主要好处是React确保它可以与服务器渲染一起使用。在服务器渲染期间,你的组件会生成HTML输出。随后,在客户端,水合会将你的事件处理程序附加到生成的HTML上。为了使水合正常工作,客户端输出必须与服务器HTML匹配。

🌐 The primary benefit of useId is that React ensures that it works with server rendering. During server rendering, your components generate HTML output. Later, on the client, hydration attaches your event handlers to the generated HTML. For hydration to work, the client output must match the server HTML.

用递增计数器很难保证这一点,因为客户端组件的水合顺序可能与服务器 HTML 发出的顺序不匹配。通过调用 useId,你可以确保水合能够正常工作,并且服务器和客户端之间的输出将保持一致。

🌐 This is very difficult to guarantee with an incrementing counter because the order in which the Client Components are hydrated may not match the order in which the server HTML was emitted. By calling useId, you ensure that hydration will work, and the output will match between the server and the client.

在 React 内部,useId 是由调用组件的“父路径”生成的。这就是为什么如果客户端和服务器的组件树相同,“父路径”无论渲染顺序如何都会匹配的原因。

🌐 Inside React, useId is generated from the “parent path” of the calling component. This is why, if the client and the server tree are the same, the “parent path” will match up regardless of rendering order.


🌐 Generating IDs for several related elements

如果你需要给多个相关元素分配 ID,你可以调用 useId 为它们生成一个共享的前缀:

🌐 If you need to give IDs to multiple related elements, you can call useId to generate a shared prefix for them:

import { useId } from 'react';

export default function Form() {
  const id = useId();
  return (
    <form>
      <label htmlFor={id + '-firstName'}>First Name:</label>
      <input id={id + '-firstName'} type="text" />
      <hr />
      <label htmlFor={id + '-lastName'}>Last Name:</label>
      <input id={id + '-lastName'} type="text" />
    </form>
  );
}

这可以让你避免对每个需要唯一 ID 的元素都调用 useId

🌐 This lets you avoid calling useId for every single element that needs a unique ID.


为所有生成的 ID 指定共享前缀

🌐 Specifying a shared prefix for all generated IDs

如果在单个页面上渲染多个独立的 React 应用,请将 identifierPrefix 作为选项传递给你的 createRoothydrateRoot 调用。这可以确保两个不同应用生成的 ID 永不冲突,因为使用 useId 生成的每个标识符都会以你指定的不同前缀开头。

🌐 If you render multiple independent React applications on a single page, pass identifierPrefix as an option to your createRoot or hydrateRoot calls. This ensures that the IDs generated by the two different apps never clash because every identifier generated with useId will start with the distinct prefix you’ve specified.

import { createRoot } from 'react-dom/client';
import App from './App.js';
import './styles.css';

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 />);


客户端和服务器端使用相同的 ID 前缀

🌐 Using the same ID prefix on the client and the server

如果你在同一页面上渲染多个独立的 React 应用,并且其中一些应用是服务器渲染的,请确保你在客户端对hydrateRoot调用传递的identifierPrefix与传递给服务器 API(例如renderToPipeableStream)的identifierPrefix相同

🌐 If you render multiple independent React apps on the same page, and some of these apps are server-rendered, make sure that the identifierPrefix you pass to the hydrateRoot call on the client side is the same as the identifierPrefix you pass to the server APIs such as renderToPipeableStream.

// Server
import { renderToPipeableStream } from 'react-dom/server';

const { pipe } = renderToPipeableStream(
<App />,
{ identifierPrefix: 'react-app1' }
);
// Client
import { hydrateRoot } from 'react-dom/client';

const domNode = document.getElementById('root');
const root = hydrateRoot(
domNode,
reactNode,
{ identifierPrefix: 'react-app1' }
);

如果页面上只有一个 React 应用,你不需要传递 identifierPrefix

🌐 You do not need to pass identifierPrefix if you only have one React app on the page.