Hook18.0+
useId
生成唯一的安全 ID,用于无障碍属性和 SSR 兼容
核心概述
HTML 的无障碍属性(如 aria-describedby)需要引用唯一的 ID。 但在 React 组件中硬编码 ID 是不行的,因为组件可能在页面上渲染多次,导致 ID 冲突。 使用 Math.random() 或计数器在 服务端渲染(SSR) 场景下又会导致客户端和服务端生成的 ID 不一致(Hydration Mismatch)。
useId 就是为了解决这个问题而生的。它能生成在客户端和服务端都保持一致的唯一 ID, 并且保证在同一组件的多次实例中互不冲突。
💡 为什么不使用自增计数器?
简单的全局自增计数器(如 let id = 0; id++)在 SSR 中会失败:
- 服务端处理请求的顺序可能与客户端渲染的顺序不同
- React 的并发模式(Concurrent Mode)可能会打乱渲染顺序
- useId 使用组件树的层级结构生成 ID,保证了稳定性和一致性
技术规格
类型签名
TypeScript
const id = useId();返回值
返回一个唯一的字符串 ID,格式通常为 :r0:, :r1: 等。注意:包含冒号。
运行机制
- SSR 兼容: 只要组件树结构一致,服务端和客户端生成的 ID 就一致
- 唯一性: 整个 React 应用内部唯一
- 格式: 包含冒号,用于确保在多根节点应用中的唯一性
实战演练
1. 基础用法:无障碍关联
将 aria-describedby 与说明文本的 ID 关联。
TypeScript
import { useId } from 'react';
function PasswordField() {
const passwordHintId = useId();
return (
<div className="flex flex-col gap-2">
<label className="font-semibold">
密码:
<input
type="password"
aria-describedby={passwordHintId}
className="ml-2 border rounded px-2 py-1"
/>
</label>
<p id={passwordHintId} className="text-sm text-gray-600">
密码应包含至少18个字符
</p>
</div>
);
}2. 为多个元素生成 ID
不需要为每个元素调用一次 useId,可以使用同一个 ID 添加后缀。
TypeScript
import { useId } from 'react';
function NameForm() {
const id = useId();
return (
<form className="p-4 border rounded">
<div className="mb-4">
<label htmlFor={id + '-firstName'} className="block mb-1">名字</label>
<input
id={id + '-firstName'}
type="text"
className="border rounded px-2 py-1 w-full"
/>
</div>
<div>
<label htmlFor={id + '-lastName'} className="block mb-1">姓氏</label>
<input
id={id + '-lastName'}
type="text"
className="border rounded px-2 py-1 w-full"
/>
</div>
</form>
);
}避坑指南
❌ 陷阱 1: 用作列表 key
不要试图用 useId 为列表项生成 key。Key 必须基于数据,而 useId 是基于组件在树中的位置。 且 Hooks 不能在循环中调用。
❌ 错误代码
TypeScript
const items = [
{ id: 1, text: 'Item A' },
{ id: 2, text: 'Item B' },
];
function List() {
const id = useId(); // ❌ 这里生成 ID 是没用的
return (
<ul>
{items.map(item => (
// ❌ 错误:不要使用 useId 生成 key
<li key={useId()}>{item.text}</li>
))}
</ul>
);
}
// 原因:key 必须基于数据,且在其生命周期内保持稳定。
// useId 的返回值虽然稳定,但不应该在循环中调用 Hooks。✅ 修正代码
TypeScript
const items = [
{ id: 1, text: 'Item A' },
{ id: 2, text: 'Item B' },
];
function List() {
return (
<ul>
{items.map(item => (
// ✅ 正确:使用数据中的唯一 ID
<li key={item.id}>{item.text}</li>
))}
</ul>
);
}❌ 陷阱 2: 用作 CSS 选择器
useId 返回的字符串包含冒号(如 :r1:),这在 CSS 选择器中是特殊字符, 直接使用 querySelector 或 CSS ID 选择器会失败。
❌ 错误代码
TypeScript
function ValidatedInput() {
const id = useId();
// ❌ useId 返回包含冒号的字符串(如 ":r0:"),
// 在 CSS 选择器中是无效的(除非转义)
// document.querySelector('#' + id) 会报错
return <input id={id} />;
}✅ 修正代码
TypeScript
import { useId, useEffect, useRef } from 'react';
function ValidatedInput() {
const id = useId();
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// ✅ 正确:使用 ref 直接操作 DOM,不需要 ID 选择器
const input = inputRef.current;
if (input) {
input.focus();
}
}, []);
return <input ref={inputRef} id={id} />;
}最佳实践
1. 多应用共存配置
如果在同一个页面上有多个 React 根节点(微前端架构),可以通过 identifierPrefix 避免 ID 冲突。
TypeScript
import { createRoot } from 'react-dom/client';
const root1 = createRoot(document.getElementById('root1'), {
identifierPrefix: 'app1-'
});
root1.render(<App />);
const root2 = createRoot(document.getElementById('root2'), {
identifierPrefix: 'app2-'
});
root2.render(<App />);
// 结果:
// App1 中的 useId() -> ":app1-r0:"
// App2 中的 useId() -> ":app2-r0:"
// 避免了两个 React 应用在同一页面上的 ID 冲突2. 封装可复用组件
在基础 UI 组件中使用 useId,确保每次使用都有唯一 ID,无需像传统方式那样手动传入 id prop。
TypeScript
function TextField({ label, ...props }) {
const id = useId();
return (
<div className="mb-4">
<label htmlFor={id} className="block text-sm font-medium mb-1">
{label}
</label>
<input
id={id}
{...props}
className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
);
}
// 使用
function Form() {
return (
<form>
<TextField label="用户名" name="username" />
<TextField label="密码" type="password" name="password" />
</form>
);
}3. 处理单选组
对于 RadioGroup 这样的组件,利用前缀生成相关联的一组 ID。
TypeScript
function RadioGroup({ options, name, legend }) {
const prefix = useId();
return (
<fieldset className="border p-4 rounded">
<legend className="font-semibold mb-2">{legend}</legend>
{options.map((option, index) => {
// ✅ 为每个选项生成唯一 ID
const optionId = `${prefix}-${index}`;
return (
<div key={option.value} className="flex items-center gap-2 mb-1">
<input
id={optionId}
name={name}
type="radio"
value={option.value}
/>
<label htmlFor={optionId}>{option.label}</label>
</div>
);
})}
</fieldset>
);
}4. 结合 TypeScript 使用
useId 的类型定义简单直接,返回 string 类型。
TypeScript
function Component() {
const id: string = useId(); // TypeScript 知道返回类型是 string
// 可以安全地用于所有需要字符串 ID 的场景
return (
<div>
<label htmlFor={id}>Label</label>
<input id={id} />
</div>
);
}useId vs 其他 ID 生成方法对比
| 方法 | SSR 兼容 | 唯一性 | 稳定性 | 推荐度 |
|---|---|---|---|---|
useId() | ✅ 完美兼容 | ✅ 组件树内唯一 | ✅ 生命周期内稳定 | ⭐⭐⭐⭐⭐ |
Math.random() | ❌ SSR 错误 | ⚠️ 全局不保证 | ❌ 每次渲染变化 | ⭐ |
Date.now() | ❌ SSR 错误 | ⚠️ 同时调用冲突 | ❌ 每次渲染变化 | ⭐ |
| 全局计数器 | ❌ SSR 不同步 | ✅ 全局唯一 | ✅ 稳定 | ⭐⭐ |
| UUID 库 | ❌ SSR 错误 | ✅ 全局唯一 | ❌ 每次渲染变化 | ⭐⭐ |