為了生成唯一id,React18專門引入了新Hook:useId
大家好,我卡頌。
看看如下組件有什么問題:
- // App.tsx
- const id = Math.random();
- export default function App() {
- return <div id={id}>Hello</div>
- }
如果應(yīng)用是CSR(客戶端渲染),id是穩(wěn)定的,App組件沒有問題。
但如果應(yīng)用是SSR(服務(wù)端渲染),那么App.tsx會經(jīng)歷:
- React在服務(wù)端渲染,生成隨機(jī)id(假設(shè)為0.1234),這一步叫dehydrate(脫水)
- <div id="0.12345">Hello</div>作為HTML傳遞給客戶端,作為首屏內(nèi)容
- React在客戶端渲染,生成隨機(jī)id(假設(shè)為0.6789),這一步叫hydrate(注水)
客戶端、服務(wù)端生成的id不匹配!
事實(shí)上,服務(wù)端、客戶端無法簡單生成穩(wěn)定、唯一的id是個(gè)由來已久的問題,早在15年就有人提過issue:
Generating random/unique attributes server-side that don't break client-side mounting[1]
直到最近,React18推出了官方Hook——useId,才解決以上問題。他的用法很簡單:
- function Checkbox() {
- // 生成唯一、穩(wěn)定id
- const id = useId();
- return (
- <>
- <label htmlFor={id}>Do you like React?</label>
- <input type="checkbox" name="react" id={id} />
- </>
- );
- );
雖然用法簡單,但背后的原理卻很有意思 —— 每個(gè)id代表該組件在組件樹中的層級結(jié)構(gòu)。
本文讓我們來了解useId的原理。
React18來了,一切都變了
這個(gè)問題雖然一直存在,但之前一直可以使用自增的全局計(jì)數(shù)變量作為id,考慮如下例子:
- // 全局通用的計(jì)數(shù)變量
- let globalIdIndex = 0;
- export default function App() {
- const id = useState(() => globalIdIndex++);
- return <div id={id}>Hello</div>
- }
只要React在服務(wù)端、客戶端的運(yùn)行流程一致,那么雙端產(chǎn)生的id就是對應(yīng)的。
但是,隨著React Fizz(React新的服務(wù)端流式渲染器)的到來,渲染順序不再一定。
比如,有個(gè)特性叫 Selective Hydration,可以根據(jù)用戶交互改變hydrate的順序。
當(dāng)下圖左側(cè)部分在hydrate時(shí),用戶點(diǎn)擊了右下角部分:
此時(shí)React會優(yōu)先對右下角部分hydrate:
關(guān)于Selective Hydration更詳細(xì)的解釋見:New Suspense SSR Architecture in React 18[2]
如果應(yīng)用中使用自增的全局計(jì)數(shù)變量作為id,那么顯然先hydrate的組件id會更小,所以id是不穩(wěn)定的。
那么,有沒有什么是服務(wù)端、客戶端都穩(wěn)定的標(biāo)記呢?
答案是:組件的層次結(jié)構(gòu)。
useId的原理
假設(shè)應(yīng)用的組件樹如下圖:
不管B和C誰先hydrate,他們的層級結(jié)構(gòu)是不變的,所以「層級」本身就能作為服務(wù)端、客戶端之間不變的標(biāo)識。
比如B可以使用2-1作為id,C使用2-2作為id:
- function B() {
- // id為"2-1"
- const id = useId();
- return <div id={id}>B</div>;
- }
實(shí)際需要考慮兩個(gè)要素:
1. 同一個(gè)組件使用多個(gè)id
比如這樣:
- function B() {
- const id0 = useId();
- const id1 = useId();
- return (
- <ul>
- <li id={id0}></li>
- <li id={id1}></li>
- </ul>
- );
- }
2. 要跳過沒有使用useId的組件
還是考慮這個(gè)組件樹結(jié)構(gòu):
如果組件A、D使用了useId,B、C沒有使用,那么只需要為A、D劃定層級,這樣就能「減少需要表示層級」。
在useId的實(shí)際實(shí)現(xiàn)中,層級被表示為「32進(jìn)制」的數(shù)。
之所以選擇「32進(jìn)制」,是因?yàn)檫x擇盡可能大的進(jìn)制會讓生成的字符串盡可能緊湊。比如:
- const a = 18;
- // "10010" length 5
- a.toString(2)
- // "i" length 1
- a.toString(32)
具體的useId層級算法參考useId[3]
總結(jié)
React源碼內(nèi)部有多種棧結(jié)構(gòu)(比如用于保存context數(shù)據(jù)的棧)。
useId 棧的邏輯是其中比較復(fù)雜的一種。
誰能想到用法如此簡單的API背后,實(shí)現(xiàn)起來居然這么復(fù)雜?
React團(tuán)隊(duì)搗鼓「并發(fā)特性」,真挺不容易的...
參考資料
[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