大家好,我卡颂。
看看如下组件有什么问题:
`// App.tsx
const id = Math.random();
export default function App() {
return <div id={id}>Hello</div>
}
`
如果应用是CSR
(客户端渲染),id
是稳定的,App
组件没有问题。
但如果应用是SSR
(服务端渲染),那么App.tsx
会经历:
React
在服务端渲染,生成随机id
(假设为0.1234
),这一步叫dehydrate
(脱水)
<div id="0.12345">Hello</div>
作为HTML
传递给客户端,作为首屏内容
React
在客户端渲染,生成随机id
(假设为0.6789
),这一步叫hydrate
(注水)
客户端、服务端生成的id
不匹配!
事实上,服务端、客户端无法简单生成稳定、唯一的id
是个由来已久的问题,早在15年就有人提过issue
:
Generating random/unique attributes server-side that don’t break client-side mounting[1]
直到最近,React18
推出了官方Hook
——useId
,才解决以上问题。他的用法很简单:
`function Checkbox() {
// 生成唯一、稳定id
const id = useId();
return (
<>
<label htmlFor={id}>Do you like React?</label>
<input type="checkbox" name="react" id={id} />
</>
);
);
`
虽然用法简单,但背后的原理却很有意思 —— 每个id
代表该组件在组件树中的层级结构。
本文让我们来了解useId
的原理。
React18来了,一切都变了
这个问题虽然一直存在,但之前一直可以使用自增的全局计数变量
作为id
,考虑如下例子:
`// 全局通用的计数变量
let globalIdIndex = 0;
export default function App() {
const id = useState(() => globalIdIndex++);
return <div id={id}>Hello</div>
}
`
只要React
在服务端、客户端的运行流程一致,那么双端产生的id
就是对应的。
但是,随着React Fizz
(React
新的服务端流式渲染器)的到来,渲染顺序不再一定。
比如,有个特性叫 Selective Hydration
,可以根据用户交互改变hydrate
的顺序。
当下图左侧部分在hydrate
时,用户点击了右下角部分:
此时React
会优先对右下角部分hydrate
:
关于Selective Hydration
更详细的解释见:New Suspense SSR Architecture in React 18[2]
如果应用中使用自增的全局计数变量
作为id
,那么显然先hydrate
的组件id
会更小,所以id
是不稳定的。
那么,有没有什么是服务端、客户端都稳定的标记呢?
答案是:组件的层次结构。
useId的原理
假设应用的组件树如下图:
不管B
和C
谁先hydrate
,他们的层级结构是不变的,所以「层级」本身就能作为服务端、客户端之间不变的标识。
比如B
可以使用2-1
作为id
,C
使用2-2
作为id
:
`function B() {
// id为"2-1"
const id = useId();
return <div id={id}>B</div>;
}
`
实际需要考虑两个要素:
1. 同一个组件使用多个id
比如这样:
`function B() {
const id0 = useId();
const id1 = useId();
return (
<ul>
<li id={id0}></li>
<li id={id1}></li>
</ul>
);
}
`
2. 要跳过没有使用useId的组件
还是考虑这个组件树结构:
如果组件A
、D
使用了useId
,B
、C
没有使用,那么只需要为A
、D
划定层级,这样就能「减少需要表示层级」。
在useId
的实际实现中,层级被表示为「32进制」的数。
之所以选择「32进制」,是因为选择尽可能大的进制会让生成的字符串尽可能紧凑。比如:
`const a = 18;
// "10010" length 5
a.toString(2)
// "i" length 1
a.toString(32)
`
总结
React
源码内部有多种栈
结构(比如用于保存context
数据的栈
)。
useId
栈
的逻辑是其中比较复杂的一种。
谁能想到用法如此简单的API
背后,实现起来居然这么复杂?
React
团队捣鼓「并发特性」,真挺不容易的…
参考资料
[1]
Generating random/unique attributes server-side that don’t break client-side mounting:https://github.com/facebook/react/issues/4000
[2]
New Suspense SSR Architecture in React 18:https://github.com/reactwg/react-18/discussions/37
[3]
useId:https://github.com/facebook/react/pull/22644