useId 为组件的每个实例返回一个唯一的 id,并且每次渲染都返回同一个 id。当组件可能在页面上出现不止一次时,用它把 <label> 和对应的 <input> 关联起来,或者通过 aria-describedby 把 input 和它的提示关联起来。
同一个组件渲染了两次,得到两个不同的 id,分别显示在每个字段下方。点击文字“Confirm password”,焦点会跳到第二个输入框,因为它的 htmlFor 只与那个输入框的 id 匹配。如果把 useId() 换成写死的 id="password",两个 label 都会指向第一个输入框。
语法
const id = useId();
useId 不接收参数,返回一个字符串。和所有 Hook 一样,在组件的顶层调用它。它的具体格式属于内部实现,在不同版本之间变过:React 18 生成 :r1:;React 19.2 中,首次在浏览器中渲染的组件得到 _r_1_,在服务器上渲染的组件得到一个以 _R_ 开头、根据树中位置生成的 id。永远不要解析它,也不要依赖它的格式。
为什么不用 Math.random 或计数器
用于无障碍访问的 id,必须在服务器发送的 HTML 和 React 在浏览器中构建的树之间保持一致。最容易想到的两种生成 id 的方法都过不了这一关。
// Changes on every render, and differs between server and browser
const id = 'field-' + Math.random().toString(36).slice(2);
// The server's counter keeps growing across requests, the browser starts at 0
let nextId = 0;
const id = 'field-' + nextId++;
在服务端渲染时(Next.js、React Router 的框架模式,或任何使用 hydrateRoot 的项目),服务器在 HTML 中输出 id="field-4817",浏览器第一次渲染算出的是 field-0,React 就会报告 hydration 不匹配。useId 根据组件在树中的位置生成 id,而这个位置在两边完全相同。
即使没有服务器,在渲染期间生成的 id 也可能每次渲染都变。下面这个例子不涉及任何服务器,就能看出区别:
多点几次按钮。useId 的值保持不变,而计数器生成的 id 每渲染一次就增加一次,于是原本指向旧 id 的东西(aria-describedby、label)现在什么都指不到了。把计数器包进 useState(() => nextId++) 能解决重新渲染的问题,但解决不了服务端不匹配的问题。
一次调用得到多个 id
一个有多个字段的组件不需要调用多次 useId。生成一个基础 id,再给每个元素加上后缀即可。
在邮箱字段里输入一个不含 @ 的词:错误提示会出现,并且输入框的 aria-describedby 指向它,这样当输入框获得焦点时,屏幕阅读器会读出这条错误。在 App 中渲染两次 <SignupForm />,每一份都会得到自己的基础 id。
不能用作列表的 key
key 和 id 解决的是不同的问题。key 告诉 React 在多次渲染之间哪一项对应哪一项,所以它必须来自数据。useId 为每个组件实例提供一个 id,而且你不能在 map 里调用它。
// Wrong: breaks the rules of hooks, and the key is unrelated to the item
{todos.map((todo) => <Todo key={useId()} todo={todo} />)}
// Right: the key comes from the data
{todos.map((todo) => <Todo key={todo.id} todo={todo} />)}
如果你的数据没有 id,就在创建这一项的时候生成一个(在添加它的事件处理函数中调用 crypto.randomUUID()),而不是在渲染期间生成。列表与 key 一页解释了为什么 key 必须始终跟随它对应的那一项。
同一页面上有多个 React 根
如果同一个页面上渲染了两个独立的 React 应用,它们的 id 可能会冲突。给每个根加一个前缀:
createRoot(document.getElementById('cart'), { identifierPrefix: 'cart-' });
createRoot(document.getElementById('chat'), { identifierPrefix: 'chat-' });
在服务端渲染时,把同一个 identifierPrefix 同时传给服务端渲染器和 hydrateRoot,这样两边生成的 id 才会一致。
常见错误
在服务器和浏览器中渲染不同的树。 useId 依赖组件的位置,所以如果在某个字段上方有 typeof window === 'undefined' ? <A /> : <B /> 这样的分支,两次渲染中的 id 就可能错位。hydration 期间保持树结构一致,等它完成后再在 Effect 中切换。
通过 id 查找元素。 document.getElementById(id) 能用,但 ref 才是 React 访问 DOM 节点的方式,而且根本不需要 id。
把它当作随机值使用。 这个 id 在应用内是唯一的,但不是随机的,可以根据树结构推算出来。不要把它用作安全令牌、缓存键,或任何跨会话保存的东西。
什么时候使用
只要可复用的组件需要 id 属性,就使用 useId:由一个会多次使用的组件构成的表单字段,通过 aria-describedby 关联的提示框,带 aria-labelledby 的对话框,带 aria-controls 的选项卡。如果 id 只是用来把 label 和输入框关联起来,也可以不用 id,直接把输入框嵌套在 label 里面(<label>Name <input /></label>);当元素无法嵌套时再用 useId。
常见问题
React 中的 useId 有什么用?
它返回一个字符串,这个字符串在该组件实例中是唯一的,并且每次渲染都保持不变。你用它通过 id 把元素关联起来:label 上的 htmlFor,input 上的 aria-describedby,对话框上的 aria-labelledby。
为什么不用 Math.random() 或计数器生成 id?
它们在服务器和浏览器中得到的值不同,于是服务端渲染的页面和 hydration 之后的版本对不上,React 会报告 hydration 不匹配。Math.random() 还会在每次渲染时变化。useId 根据组件在树中的位置生成 id,而这个位置在两边是相同的。
可以用 useId 作为列表的 key 吗?
不可以。key 必须来自你的数据,这样 React 才能在多次渲染之间匹配到同一项。useId 每个组件只调用一次,而且在 map 里调用它本身就违反了 Hook 的规则。请使用每一项自己的 id。
怎样用一次 useId 调用得到多个 id?
调用一次 useId,然后加上后缀:${id}-name、${id}-email。基础 id 是唯一的,所以加了后缀的字符串也是唯一的。
可以在 CSS 选择器中使用 useId 生成的 id 吗?
尽量不要。这个 id 是用来在 DOM 中关联元素的,它的具体格式属于内部细节,在不同 React 版本之间变过。样式请用 class,查找元素请用 ref,而不是 querySelector。