為了生成唯一id,React18專門引入了新Hook:useId

卡頌 發表於 2021-11-30
React

大家好,我卡頌。

看看如下元件有什麼問題:

// 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時,使用者點選了右下角部分:

為了生成唯一id,React18專門引入了新Hook:useId

此時React會優先對右下角部分hydrate

為了生成唯一id,React18專門引入了新Hook:useId

關於Selective Hydration更詳細的解釋見:New Suspense SSR Architecture in React 18

如果應用中使用自增的全域性計數變數作為id,那麼顯然先hydrate的元件id會更小,所以id是不穩定的。

那麼,有沒有什麼是服務端、客戶端都穩定的標記呢?

答案是:元件的層次結構。

useId的原理

假設應用的元件樹如下圖:

為了生成唯一id,React18專門引入了新Hook: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的元件

還是考慮這個元件樹結構:

為了生成唯一id,React18專門引入了新Hook: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團隊搗鼓併發特性,真挺不容易的...