為了生成唯一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 團隊搗鼓 併發特性 ,真挺不容易的...