为了生成唯一id,React18专门引入了新Hook:useId

开发 前端
直到最近,React18推出了官方Hook——useId,虽然用法简单,但背后的原理却很有意思 —— 每个id代表该组件在组件树中的层级结构。

[[437140]]

大家好,我卡颂。

看看如下组件有什么问题:

  1. // App.tsx 
  2. const id = Math.random(); 
  3.  
  4. export default function App() { 
  5.   return <div id={id}>Hello</div> 

如果应用是CSR(客户端渲染),id是稳定的,App组件没有问题。

但如果应用是SSR(服务端渲染),那么App.tsx会经历:

  1. React在服务端渲染,生成随机id(假设为0.1234),这一步叫dehydrate(脱水)
  2. <div id="0.12345">Hello</div>作为HTML传递给客户端,作为首屏内容
  3. 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,才解决以上问题。他的用法很简单:

  1. function Checkbox() { 
  2.   // 生成唯一、稳定id 
  3.   const id = useId(); 
  4.   return ( 
  5.     <> 
  6.       <label htmlFor={id}>Do you like React?</label> 
  7.       <input type="checkbox" name="react" id={id} /> 
  8.     </> 
  9.   ); 
  10. ); 

虽然用法简单,但背后的原理却很有意思 —— 每个id代表该组件在组件树中的层级结构。

本文让我们来了解useId的原理。

React18来了,一切都变了

这个问题虽然一直存在,但之前一直可以使用自增的全局计数变量作为id,考虑如下例子:

  1. // 全局通用的计数变量 
  2. let globalIdIndex = 0; 
  3.  
  4.  
  5. export default function App() { 
  6.   const id = useState(() => globalIdIndex++); 
  7.   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:

  1. function B() { 
  2.   // id为"2-1" 
  3.   const id = useId(); 
  4.   return <div id={id}>B</div>; 

实际需要考虑两个要素:

1. 同一个组件使用多个id

比如这样:

  1. function B() { 
  2.   const id0 = useId(); 
  3.   const id1 = useId(); 
  4.   return ( 
  5.     <ul> 
  6.       <li id={id0}></li> 
  7.       <li id={id1}></li> 
  8.     </ul> 
  9.   ); 

2. 要跳过没有使用useId的组件

还是考虑这个组件树结构:

如果组件A、D使用了useId,B、C没有使用,那么只需要为A、D划定层级,这样就能「减少需要表示层级」。

在useId的实际实现中,层级被表示为「32进制」的数。

之所以选择「32进制」,是因为选择尽可能大的进制会让生成的字符串尽可能紧凑。比如:

  1. const a = 18; 
  2.  
  3. // "10010" length 5 
  4. a.toString(2)    
  5.  
  6. //  "i" length 1 
  7. a.toString(32)   

具体的useId层级算法参考useId[3]

总结

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

 

责任编辑:姜华 来源: 魔术师卡颂
相关推荐

2021-11-01 19:49:55

React组件模式

2021-06-22 07:45:57

React18startTransiReact

2021-06-22 07:30:07

React18Automatic b自动批处理

2021-06-16 06:05:25

React18React

2022-03-16 17:01:35

React18并发的React组件render

2022-03-30 14:22:55

ReactReact18并发特性

2023-03-21 08:31:13

ReconcilerFiber架构

2022-02-28 10:30:03

架构代码Native

2022-04-27 07:37:42

ReactReact18

2022-02-23 07:09:30

分布式ID雪花算法

2022-05-16 08:00:55

ReactReact 18数组

2022-10-14 08:45:54

2023-03-28 07:59:57

ReactReconciler

2021-08-22 17:27:50

KDE PlasmaWindows概览效果

2022-04-18 08:57:32

React 18前端

2022-07-06 15:07:47

React开发

2022-03-25 08:31:09

ReactReact 18升级

2020-07-21 11:35:21

开发技能代码

2021-11-08 19:25:37

Go生成系统

2022-07-03 20:53:23

React18请求数据
点赞
收藏

51CTO技术栈公众号