大家好,我卡頌。
看看如下元件有什麼問題:
// 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 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 Fizz
(React
新的服務端流式渲染器)的到來,渲染順序不再一定。
比如,有個特性叫 Selective Hydration
,可以根據使用者互動改變hydrate
的順序。
當下圖左側部分在hydrate
時,使用者點選了右下角部分:
此時React
會優先對右下角部分hydrate
:
關於Selective Hydration
更詳細的解釋見:New Suspense SSR Architecture in React 18
如果應用中使用自增的全域性計數變數
作為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
總結
React
原始碼內部有多種棧
結構(比如用於儲存context
資料的棧
)。
useId
棧
的邏輯是其中比較複雜的一種。
誰能想到用法如此簡單的API
背後,實現起來居然這麼複雜?
React
團隊搗鼓併發特性,真挺不容易的...