useId 源码解析
这是在 React v18 版本中新增的一个 hooks。它的出现是为了解决一些在 SSR 场景下,因为服务端生成随机 id 和客户端生成随机 id 不一致的问题,导致了客户端重新渲染
重要
useId 是一个用于生成横跨服务端和客户端的稳定的唯一 ID 的同时避免 hydration 不匹配的 hook。
使用场景
如果是 CSR 场景,id 是稳定的,不会有什么问题出现
在 useId 出现之前,会有这样的情况出现
const id = Math.random();
export default function App() {
return <div id={id}>Hello useId</div>
}
当我们采用 Math.random()
来生成随机 id 时,由于采用的 SSR,在服务端会生成一次随机 id,将 jsx
转成 html
string 传递给客户端,作为首屏渲染
在客户端渲染时,生成随机 id,这一步叫 hydrate
客户端和服务端产生的 Id 不一致
useId 的诞生就是为了解决这个问题,那么是如何使用的呢?
使用方式
一个最简单的例子,只需要使用 useId
来生成稳定的 Id,直接传递 id 给需要它的元素即可,使用起来非常方便简单
const id = useId()
export default function App() {
return <div id={id}>Hello useId</div>
}
对于同一组件中的多个 ID,使用相同的 id 并添加后缀:
function NameFields() {
const id = useId();
return (
<div>
<label htmlFor={id + '-firstName'}>First Name</label>
<div>
<input id={id + '-firstName'} type="text" />
</div>
<label htmlFor={id + '-lastName'}>Last Name</label>
<div>
<input id={id + '-lastName'} type="text" />
</div>
</div>
);
}
那么 useId
生成的 id 是怎么样的呢?
生成的 Id 是带 :
的,用来确保唯一,毕竟很少人用 :
做 id
重要
useId 生成一个包含 : 的字符串 token。这有助于确保 token 是唯一的,但在 CSS 选择器或 querySelectorAll
等 API 中不受支持。
和其他的 hook 一样,执行useId
会在 mount
阶段会调用 mountId
,在 update
阶段会调用 updateId
先来看看 mountId
mount 阶段
可以看到 mountId
会做以下几件事情
- 创建 hook 对象
- 获取当前组件的 root Fiber 上的
identifierPrefix
id 前缀 - 判断是 SSR 还是 CSR
- SSR 会根据 Tree 来生成 Id,并夹杂大写字母 R
- CSR 会根据一个全局变量来生成自增的 Id,夹杂小写字母 r
- 最后挂载到
memoizedState
上
function mountId(): string {
const hook = mountWorkInProgressHook();
const root = ((getWorkInProgressRoot(): any): FiberRoot);
const identifierPrefix = root.identifierPrefix;
let id;
if (getIsHydrating()) {
const treeId = getTreeId();
// Use a captial R prefix for server-generated ids.
id = ':' + identifierPrefix + 'R' + treeId;
const localId = localIdCounter++;
if (localId > 0) {
id += 'H' + localId.toString(32);
}
id += ':';
} else {
// Use a lowercase r prefix for client-generated ids.
const globalClientId = globalClientIdCounter++;
id = ':' + identifierPrefix + 'r' + globalClientId.toString(32) + ':';
}
hook.memoizedState = id;
return id;
}
注意
identifierPrefix
: React 用于生成的 id 的可选前缀,在同一页面上使用多个根时避免冲突很有用。必须与服务器上使用的前缀相同。
hydrateRoot(container, element[, options])
写在 options 里的
重点需要放到这个 getTreeId
的方法上,这个函数究竟是如何工作的呢?
getTreeId
getTreeId 使用组件的树状结构(在客户端和服务端都绝对稳定)来生成id。这里涉及到了 React 的 Id 生成算法具体可以看这个 PR
为了让 Id 更加的紧凑,并不是所有的组件都会生成 ID,只有调用了 useId
的组件才会生成 Id,并且 Id 是连续的,如果有其中一层组件没有调用 useId
那就跳过这一层
在 useId 的实际实现中,层级被表示为32进制的数。
如果 组件的层数超过了 32 进制数能表达的数时,会通过 treeContextOverflow
来将超出的几位截断,转成字符串,继续拼接在 id 的后面
export function getTreeId(): string {
const overflow = treeContextOverflow;
const idWithLeadingBit = treeContextId;
const id = idWithLeadingBit & ~getLeadingBit(idWithLeadingBit);
return id.toString(32) + overflow;
}
update 阶段
update 时,不需要做什么事情,获取 id 返回即可
function updateId(): string {
const hook = updateWorkInProgressHook();
const id: string = hook.memoizedState;
return id;
}