
React useId 实战:治好 SSR 水合警告、表单无障碍与唯一 ID 生成你给一个输入框配 label,想用htmlFor关联,于是随手写了个 id:function EmailField() { return ( label htmlForemail邮箱/label input idemail / / ); }单个用没问题。可这个组件一旦在页面里出现两次,DOM 里就有两个idemail——点第二个 label 会聚焦到第一个 input,无障碍读屏也乱套。你想到用随机数生成 id,结果 Next.js 服务端渲染立刻甩你一脸红色警告:Hydration failed。React 18 的useId就是来解决这一串问题的。为什么 Math.random() 会炸水合先看错误示范:function EmailField() { const id email-${Math.random()}; // 每次渲染都变 return ( label htmlFor{id}邮箱/label input id{id} / / ); }问题的根源是 SSR 的两段式渲染:服务端渲染一次 HTML(比如 id 是email-0.123),浏览器加载后 React 再水合(hydrate)一次,又跑一遍Math.random()得到email-0.789。服务端和客户端生成的 HTML 对不上,React 检测到不一致就报Hydration failed,严重时整棵子树被丢弃重渲染,首屏闪一下。用模块级自增计数器(let seq 0; const id seq)也不行——服务端和客户端的自增起点、渲染顺序未必一致,同样水合不上。useId 的正确姿势useId生成的 id 在服务端和客户端保证一致,专为水合设计:import { useId } from react; function EmailField() { const id useId(); // SSR 与 CSR 生成同一个值,水合不再报错 return ( label htmlFor{id}邮箱/label input id{id} / / ); }现在同一个组件渲染多次,每个实例拿到的 id 都不同(React 根据组件在树中的位置生成,类似:r0:、:r1:),label 和 input 精确配对,读屏软件也能正确关联。一个组件里多个 id:加后缀,别调多次一个表单项经常需要好几个关联 id:input 本身、错误提示、帮助文本。别为每个都调一次useId,而是调一次、拼后缀:function PasswordField({ error }) { const id useId(); // 一个 useId 派生出一组稳定关联的 id const inputId ${id}-input; const errId ${id}-error; const hintId ${id}-hint; return ( div label htmlFor{inputId}密码/label input id{inputId} typepassword aria-describedby{${hintId} ${errId}} aria-invalid{!!error} / p id{hintId}至少 8 位,含字母和数字/p {error p id{errId}{error}/p} /div ); }aria-describedby把帮助文本和错误提示都关联到输入框,读屏软件聚焦时会一起念出来。这就是useId最典型的价值:无障碍属性需要稳定、唯一、SSR 安全的 id。三个必须记住的边界第一,useId 不是给列表 key 用的。这是最常见的误用:// 错误!useId 只能在组件顶层调用,不能在 map 循环里调 {items.map((item) { const id useId(); // 违反 Hooks 规则,直接报错 return li key{id}{item.name}/li; })}列表 key 要用数据本身的稳定标识(item.id),useId是给渲染无关的 DOM 关联 id用的,两者场景完全不同。第二,别拿它当数据库主键或请求参数。useId生成的值形如:r0:,带冒号,只保证在当前这次渲染的组件树里唯一且 SSR 一致。它会随组件在树中的位置变化,刷新页面也可能变,拿去当业务 id 提交给后端必然出乱子。第三,同一个 id 别跨组件传来传去当全局唯一标识。它的设计目标就是就近关联 DOM 元素,超出这个范围就是误用。配合 CSS-in-JS 的 id 选择器坑useId默认输出带冒号(:r0:),而冒号在 CSS 选择器里有特殊含义(伪类)。如果你想用生成的 id 写document.querySelector(# id)或 CSS#:r0:,会直接语法出错。解决办法是给它套个合法前缀,或用CSS.escape:const id useId(); const safeId f${id.replace(/:/g, )}; // 去掉冒号,前面加字母保证是合法选择器多数场景直接把useId()的值塞进id/htmlFor/aria-*属性即可(这些属性对冒号无所谓),只有当你要拿它做 CSS/JS 选择器时才需要清洗。小结useId生成SSR/CSR 一致的唯一 id,专治Math.random()和自增计数器导致的Hydration failed。典型场景是表单无障碍:htmlFor/aria-describedby/aria-invalid需要稳定唯一的关联 id。一个组件多个 id 时,调一次useId再拼后缀,不要循环里调,也不要为每个属性各调一次。三条红线:不做列表 key、不做业务主键/请求参数、不跨组件当全局标识。值带冒号,做 CSS/JS 选择器前要清洗;直接塞属性则无需处理。记忆点:useId 只干一件事——给同一处 DOM 的关联属性发一个 SSR 安全的唯一号。