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

语言: CN / TW / HK

大家好,我卡颂。

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

// App.tsx
const id = Math.random();

export default function App() {
  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

直到最近, 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 FizzReact 新的服务端流式渲染器)的到来,渲染顺序不再一定。

比如,有个特性叫 Selective Hydration ,可以根据用户交互改变 hydrate 的顺序。

当下图左侧部分在 hydrate 时,用户点击了右下角部分:

此时 React 会优先对右下角部分 hydrate

关于 Selective Hydration 更详细的解释见: New Suspense SSR Architecture in React 18

如果应用中使用 自增的全局计数变量 作为 id ,那么显然先 hydrate 的组件 id 会更小,所以 id 是不稳定的。

那么,有没有什么是服务端、客户端都稳定的标记呢?

答案是:组件的层次结构。

useId的原理

假设应用的组件树如下图:

不管 BC 谁先 hydrate ,他们的层级结构是不变的,所以 层级 本身就能作为服务端、客户端之间不变的标识。

比如 B 可以使用 2-1 作为 idC 使用 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的组件

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

如果组件 AD 使用了 useIdBC 没有使用,那么只需要为 AD 划定层级,这样就能 减少需要表示层级

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

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

const a = 18;

// "10010" length 5
a.toString(2)   

//  "i" length 1
a.toString(32)

具体的 useId 层级算法参考useId

总结

React 源码内部有多种 结构(比如用于保存 context 数据的 )。

useId 的逻辑是其中比较复杂的一种。

谁能想到用法如此简单的 API 背后,实现起来居然这么复杂?

React 团队捣鼓 并发特性 ,真挺不容易的...