为了生成唯一id,React18专门引入了新Hook:useId
大家好,为生我卡颂。门引
看看如下组件有什么问题:
// 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 dont 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)具体的useId层级算法参考useId[3]
总结
React源码内部有多种栈结构(比如用于保存context数据的栈)。
useId 栈的逻辑是其中比较复杂的一种。
谁能想到用法如此简单的API背后,实现起来居然这么复杂?
React团队捣鼓「并发特性」,真挺不容易的...
参考资料
[1]Generating random/unique attributes server-side that dont 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