useId

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

const id = useId()

参考

useId()

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

import { useId } from 'react';

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

查看更多示例。

参数

useId 不接受任何参数。

返回值

useId 返回一个唯一的 ID 字符串,它与这个特定组件中的这次 useId 调用相关联。

注意事项

  • useId 是一个 Hook,因此你只能在组件顶层或你自己的 Hooks 中调用它。你不能在循环或条件中调用它。如果你需要这样做,请抽取一个新组件并将状态移入其中。

  • useId 不应被用于为 use() 生成缓存键。组件挂载时该 ID 是稳定的,但在渲染过程中可能会改变。缓存键应当由你的数据生成。

  • useId 不应被用于在列表中生成键键应该由你的数据生成。

  • useId 目前不能用于 异步 Server Components


用法

Pitfall

不要调用 useId 来为列表生成键。 键应该由你的数据生成。

为无障碍属性生成唯一 ID

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

import { useId } from 'react';

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

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

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

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

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

在普通 HTML 中,你会这样写:

<label>
密码:
<input
type="password"
aria-describedby="password-hint"
/>
</label>
<p id="password-hint">
密码应至少包含 18 个字符
</p>

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

import { useId } from 'react';

function PasswordField() {
const passwordHintId = useId();
return (
<>
<label>
密码:
<input
type="password"
aria-describedby={passwordHintId}
/>
</label>
<p id={passwordHintId}>
密码应至少包含 18 个字符
</p>
</>
);
}

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

import { useId } from 'react';

function PasswordField() {
  const passwordHintId = useId();
  return (
    <>
      <label>
        密码:
        <input
          type="password"
          aria-describedby={passwordHintId}
        />
      </label>
      <p id={passwordHintId}>
        密码应至少包含 18 个字符
      </p>
    </>
  );
}

export default function App() {
  return (
    <>
      <h2>选择密码</h2>
      <PasswordField />
      <h2>确认密码</h2>
      <PasswordField />
    </>
  );
}

观看这个视频 来了解辅助技术下用户体验的差异。

Pitfall

服务端渲染中,useId 要求服务端和客户端具有完全相同的组件树。如果你在服务端和客户端渲染的树不完全匹配,生成的 ID 也不会匹配。

Deep Dive

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

你可能会想,为什么 useId 比像 nextId++ 这样的全局变量递增更好。

useId 的主要优势在于,React 确保它可以与服务端渲染配合使用。在服务端渲染期间,你的组件会生成 HTML 输出。随后在客户端,hydration 会把事件处理器附加到生成的 HTML 上。要让 hydration 正常工作,客户端输出必须与服务端 HTML 匹配。

对于递增计数器来说,要保证这一点非常困难,因为客户端组件被 hydration 的顺序可能与服务端 HTML 的输出顺序不一致。通过调用 useId,你可以确保 hydration 正常工作,并且服务端与客户端之间的输出能够匹配。

在 React 内部,useId 是从调用该 Hook 的组件的“父路径”生成的。这就是为什么如果客户端和服务端的树相同,那么无论渲染顺序如何,“父路径”都会对应起来。


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

import { useId } from 'react';

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

这样你就不必为每一个需要唯一 ID 的元素都调用 useId


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

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

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 前缀

如果你在同一页面上渲染多个独立的 React 应用,并且其中一些应用是服务端渲染的,请确保你在客户端传给 hydrateRoot 调用的 identifierPrefix 与你传给诸如 renderToPipeableStream 这类服务端 APIidentifierPrefix 相同。

// 服务端
import { renderToPipeableStream } from 'react-dom/server';

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

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

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