React Hooks 使用誤區(qū),駁官方文檔
大綱
1. 不是所有的依賴(lài)都必須放到依賴(lài)數(shù)組中
2. deps 參數(shù)不能緩解閉包問(wèn)題
2.1 正常情況下是不會(huì)有閉包問(wèn)題的
2.2 延遲調(diào)用會(huì)存在閉包問(wèn)題
3. 盡量不要用 useCallback
3.1 useCallback 大部分場(chǎng)景沒(méi)有提升性能
3.2 useCallback 讓代碼可讀性變差
4. useMemo 建議適當(dāng)使用
5. useState 的正確使用姿勢(shì)
5.1 能用其他狀態(tài)計(jì)算出來(lái)就不用單獨(dú)聲明狀態(tài)
5.2 保證數(shù)據(jù)源唯一
5.3 useState 適當(dāng)合并
6、總結(jié)
正文
作為 React Hooks 庫(kù) ahooks[1] 的作者,我應(yīng)該算一個(gè)非常非常資深的 React Hooks 用戶(hù)。在兩年多的 React Hooks 使用過(guò)程中,我越來(lái)越發(fā)現(xiàn)大家(包括我自己)對(duì) React Hooks 的使用姿勢(shì)存在很大誤區(qū),歸根到底是官方文檔的教程很不嚴(yán)謹(jǐn),存在錯(cuò)誤的指引。
1. 不是所有的依賴(lài)都必須放到依賴(lài)數(shù)組中
對(duì)于所有的 React Hooks 用戶(hù),都有一個(gè)共識(shí):“useEffect 中使用到外部變量,都應(yīng)該放到第二個(gè)數(shù)組參數(shù)中”,同時(shí)我們會(huì)安裝 eslint-plugin-react-hooks[2] 插件,來(lái)提醒自己是不是忘了某些變量。
以上共識(shí)來(lái)自官方文檔:
我愿稱(chēng)該條規(guī)則為萬(wàn)惡之源,這條規(guī)則以高亮展示,所有的新人都很重視,包括我自己。然而在實(shí)際的開(kāi)發(fā)中,發(fā)現(xiàn)事情并不是這樣的。
下面舉一個(gè)比較簡(jiǎn)單的例子,要求如下:當(dāng) props.count 和 count 變化時(shí),上報(bào)當(dāng)前所有數(shù)據(jù)。
這個(gè)例子比較簡(jiǎn)單,先貼下源碼:
function?Demo(props)?{
??const?[count,?setCount]?=?useState(0);
??const?[text,?setText]?=?useState('');
??const?[a,?setA]?=?useState('');
??useEffect(()?=>?{
????monitor(props.count,?count,?text,?a);
??},?[props.count,?count]);
??return?(
????<div>
??????<button
????????onClick={()?=>?setCount(c?=>?c?+?1)}
??????>
????????click
??????button>
??????<input?value={text}?onChange={e?=>?setText(e.target.value)}?/>
??????<input?value={a}?onChange={e?=>?setA(e.target.value)}?/>
????div>
??)
}
我們能看到示例代碼中,useEffect 是不符合 React 官方建議的,text 和 a 變量沒(méi)有放到依賴(lài)數(shù)組中,ESLint 警告如下:
那如果按照規(guī)范,我們把依賴(lài)項(xiàng)都放到第二個(gè)數(shù)組參數(shù)中,會(huì)怎樣呢?
??useEffect(()?=>?{
????monitor(props.count,?count,?text,?a);
??},?[props.count,?count,?text,?a]);
如上的代碼雖然符合了 React 官方的規(guī)范,但不滿(mǎn)足我們的業(yè)務(wù)需求了,當(dāng) text 和 a 變化時(shí),也觸發(fā)了函數(shù)執(zhí)行。
此時(shí)陷入了困境,當(dāng)滿(mǎn)足 useEffect 使用規(guī)范時(shí),業(yè)務(wù)需求就不能滿(mǎn)足了。當(dāng)滿(mǎn)足業(yè)務(wù)需求時(shí),useEffect 就不規(guī)范了。
我的建議為:
不要使用 eslint-plugin-react-hooks插件,或者可以選擇性忽略該插件的警告。只有一種情況,需要把變量放到 deps 數(shù)組中,那就是當(dāng)該變量變化時(shí),需要觸發(fā) useEffect 函數(shù)執(zhí)行。而不是因?yàn)?useEffect 中用到了這個(gè)變量!
2. deps 參數(shù)不能緩解閉包問(wèn)題
假如完全按第二個(gè)建議來(lái)寫(xiě)代碼,很多人又擔(dān)心,會(huì)不會(huì)造成一些不必要的閉包問(wèn)題?我的結(jié)論是:閉包問(wèn)題和 useEffect 的 deps 參數(shù)沒(méi)有太大關(guān)系。
比如我有一個(gè)這樣的需求:當(dāng)進(jìn)入頁(yè)面 3s 后,輸出當(dāng)前最新的 count。代碼如下:
function?Demo()?{
??const?[count,?setCount]?=?useState(0);
??useEffect(()?=>?{
????const?timer?=?setTimeout(()?=>?{
??????console.log(count)
????},?3000);
????return?()?=>?{
??????clearTimeout(timer);
????}
??},?[])
??return?(
????<button
??????onClick={()?=>?setCount(c?=>?c?+?1)}
????>
??????click
????button>
??)
}
以上代碼,實(shí)現(xiàn)了初始化 3s 后,輸出 count。但很遺憾,這里肯定會(huì)出閉包問(wèn)題,哪怕進(jìn)來(lái)之后我們多次點(diǎn)擊了 button,輸出的 count 仍然為 0。
那假如我們把 count 放到 deps 中,是不是就好了?
??useEffect(()?=>?{
????const?timer?=?setTimeout(()?=>?{
??????console.log(count)
????},?3000);
????return?()?=>?{
??????clearTimeout(timer);
????}
??},?[count])
如上代碼,此時(shí)確實(shí)沒(méi)有閉包問(wèn)題了,但在每次 count 變化時(shí),定時(shí)器卸載并重新開(kāi)始計(jì)時(shí)了,不滿(mǎn)足我們的最初需求了。
要解決的唯一辦法為:
const?[count,?setCount]?=?useState(0);
//?通過(guò)?ref?來(lái)記憶最新的?count
const?countRef?=?useRef(count);
countRef.current?=?count;
useEffect(()?=>?{
??const?timer?=?setTimeout(()?=>?{
????console.log(countRef.current)
??},?3000);
??return?()?=>?{
????clearTimeout(timer);
??}
},?[])
雖然上面的代碼,很繞,但確實(shí),只有這個(gè)解決方案。請(qǐng)記住這段代碼,功能真的很強(qiáng)大。
const?countRef?=?useRef(count);
countRef.current?=?count;
上面的例子,可以發(fā)現(xiàn),閉包問(wèn)題是不能僅僅通過(guò)遵守 React 規(guī)則來(lái)避免的。我們必須清晰的知道,在什么場(chǎng)景下會(huì)出現(xiàn)閉包問(wèn)題。
2.1 正常情況下是不會(huì)有閉包問(wèn)題的
const?[a,?setA]?=?useState(0);
const?[b,?setB]?=?useState(0);
const?c?=?a?+?b;
useEffect(()=>{
?console.log(a,?b,?c)
},?[a]);
useEffect(()=>{
?console.log(a,?b,?c)
},?[b]);
useEffect(()=>{
?console.log(a,?b,?c)
},?[c]);
在一般的使用過(guò)程中,是不會(huì)有閉包問(wèn)題的,如上代碼中,完全不會(huì)有閉包問(wèn)題,和 deps 怎么寫(xiě)沒(méi)有任何關(guān)系。
2.2 延遲調(diào)用會(huì)存在閉包問(wèn)題
在延遲調(diào)用的場(chǎng)景下,一定會(huì)存在閉包問(wèn)題。 什么是延遲調(diào)用?
使用 setTimeout、setInterval、Promise.then 等 useEffect 的卸載函數(shù)
const?getUsername?=?()?=>?{
??return?new?Promise((resolve,?reject)?=>?{
????setTimeout(()?=>?{
??????resolve('John');
????},?3000);
??})
}
function?Demo()?{
??const?[count,?setCount]?=?useState(0);
??//?setTimeout?會(huì)造成閉包問(wèn)題
??useEffect(()?=>?{
????const?timer?=?setTimeout(()?=>?{
??????console.log(count);
????},?3000);
????return?()?=>?{
??????clearTimeout(timer);
????}
??},?[])
??//?setInterval?會(huì)造成閉包問(wèn)題
??useEffect(()?=>?{
????const?timer?=?setInterval(()?=>?{
??????console.log(count);
????},?3000);
????return?()?=>?{
??????clearInterval(timer);
????}
??},?[])
??//?Promise.then?會(huì)造成閉包問(wèn)題
??useEffect(()?=>?{
????getUsername().then(()?=>?{
??????console.log(count);
????});
??},?[])
??//?useEffect?卸載函數(shù)會(huì)造成閉包問(wèn)題
??useEffect(()?=>?{
????return?()?=>?{
??????console.log(count);
????}
??},?[]);
??return?(
????<button
??????onClick={()?=>?setCount(c?=>?c?+?1)}
????>
??????click
????button>
??)
}
在以上示例代碼中,四種情況均會(huì)出現(xiàn)閉包問(wèn)題,永遠(yuǎn)輸出 0。這四種情況的根因都是一樣的,我們看一下代碼的執(zhí)行順序:
組件初始化,此時(shí) count = 0執(zhí)行 useEffect,此時(shí) useEffect 的函數(shù)執(zhí)行,JS 引用鏈記錄了對(duì) count=0的引用關(guān)系點(diǎn)擊 button,count 變化,但對(duì)之前的引用已經(jīng)無(wú)能為力了
可以看到,閉包問(wèn)題均是出現(xiàn)在延遲調(diào)用的場(chǎng)景下。解決辦法如下:
const?[count,?setCount]?=?useState(0);
//?通過(guò)?ref?來(lái)記憶最新的?count
const?countRef?=?useRef(count);
countRef.current?=?count;
useEffect(()?=>?{
??const?timer?=?setTimeout(()?=>?{
????console.log(countRef.current)
??},?3000);
??return?()?=>?{
????clearTimeout(timer);
??}
},?[])
......
通過(guò) useRef 來(lái)保證任何時(shí)候訪(fǎng)問(wèn)的 countRef.current 都是最新的,以解決閉包問(wèn)題。
到這里,我重申下我對(duì) useEffect 的建議:
只有變化時(shí),需要重新執(zhí)行 useEffect 的變量,才要放到 deps 中。而不是 useEffect 用到的變量都放到 deps 中。 在有延遲調(diào)用場(chǎng)景時(shí),可以通過(guò) ref 來(lái)解決閉包問(wèn)題。
3. 盡量不要用 useCallback
我建議在項(xiàng)目中盡量不要用 useCallback,大部分場(chǎng)景下,不僅沒(méi)有提升性能,反而讓代碼可讀性變的很差。
3.1 useCallback 大部分場(chǎng)景沒(méi)有提升性能
useCallback 可以記住函數(shù),避免函數(shù)重復(fù)生成,這樣函數(shù)在傳遞給子組件時(shí),可以避免子組件重復(fù)渲染,提高性能。
const?someFunc?=?useCallback(()=>?{
???doSomething();
},?[]);
return?<ExpensiveComponent?func={someFunc}?/>
基于以上認(rèn)知,很多同學(xué)(包括我自己)在寫(xiě)代碼時(shí),只要是個(gè)函數(shù),都加個(gè) useCallback,是你么?反正我以前是。
但我們要注意,提高性能還必須有另外一個(gè)條件,子組件必須使用了 shouldComponentUpdate 或者 React.memo 來(lái)忽略同樣的參數(shù)重復(fù)渲染。
假如 ExpensiveComponent 組件只是一個(gè)普通組件,是沒(méi)有任何用的。比如下面這樣:
const?ExpensiveComponent?=?({?func?})?=>?{
??return?(
????<div?onClick={func}>
?????hello
????div>
??)
}
必須通過(guò) React.memo 包裹 ExpensiveComponent ,才會(huì)避免參數(shù)不變的情況下的重復(fù)渲染,提高性能。
const?ExpensiveComponent?=?React.memo(({?func?})?=>?{
??return?(
????<div?onClick={func}>
?????hello
????div>
??)
})
所以,useCallback 是要和 shouldComponentUpdate/React.memo 配套使用的,你用對(duì)了嗎?當(dāng)然,我建議一般項(xiàng)目中不用考慮性能優(yōu)化的問(wèn)題,也就是不要使用 useCallback 了,除非有個(gè)別非常復(fù)雜的組件,單獨(dú)使用即可。
3.2 useCallback 讓代碼可讀性變差
我看到過(guò)一些代碼,使用 useCallback 后,大概長(zhǎng)這樣:
const?someFuncA?=?useCallback((d,?g,?x,?y)=>?{
???doSomething(a,?b,?c,?d,?g,?x,?y);
},?[a,?b,?c]);
const?someFuncB?=?useCallback(()=>?{
???someFuncA(d,?g,?x,?y);
},?[someFuncA,?d,?g,?x,?y]);
useEffect(()=>{
??someFuncB();
},?[someFuncB]);
在上面的代碼中,變量依賴(lài)一層一層傳遞,最終要判斷具體哪些變量變化會(huì)觸發(fā) useEffect 執(zhí)行,是一件很頭疼的事情。
我期望不要用 useCallback,直接裸寫(xiě)函數(shù)就好:
const?someFuncA?=?(d,?g,?x,?y)=>?{
???doSomething(a,?b,?c,?d,?g,?x,?y);
};
const?someFuncB?=?()=>?{
???someFuncA(d,?g,?x,?y);
};
useEffect(()=>{
??someFuncB();
},?[...]);
在 useEffect 存在延遲調(diào)用的場(chǎng)景下,可能造成閉包問(wèn)題,那通過(guò)咱們?nèi)f能的方法就能解決:
const?someFuncA?=?(d,?g,?x,?y)=>?{
???doSomething(a,?b,?c,?d,?g,?x,?y);
};
const?someFuncB?=?()=>?{
???someFuncA(d,?g,?x,?y);
};
+?const?someFuncBRef?=?useRef(someFuncB);
+?someFuncBRef.current?=?someFuncB;
useEffect(()=>{
+??setTimeout(()=>{
+????someFuncBRef.current();
+??},?1000)
},?[...]);
對(duì) useCallback 的建議就一句話(huà):沒(méi)事別用 useCallback。
4. useMemo 建議適當(dāng)使用
相較于 useCallback 而言,useMemo 的收益是顯而易見(jiàn)的。
//?沒(méi)有使用?useMemo
const?memoizedValue?=?computeExpensiveValue(a,?b);
//?使用?useMemo
const?memoizedValue?=?useMemo(()?=>?computeExpensiveValue(a,?b),?[a,?b]);
如果沒(méi)有使用 useMemo,computeExpensiveValue 會(huì)在每一次渲染的時(shí)候執(zhí)行。如果使用了 useMemo,只有在 a 和 b 變化時(shí),才會(huì)執(zhí)行一次 computeExpensiveValue。
這筆賬大家應(yīng)該都會(huì)算,所以我建議 useMemo 可以適當(dāng)使用。
當(dāng)然也不是無(wú)節(jié)制的使用,在很簡(jiǎn)單的基礎(chǔ)類(lèi)型計(jì)算時(shí),可能 useMemo 并不劃算。
const?a?=?1;
const?b?=?2;
const?c?=?useMemo(()=>?a?+?b,?[a,?b]);
比如上面的例子,請(qǐng)問(wèn)計(jì)算 a+b 的消耗大?還是記錄 a/b ,并比較a/b 是否變化的消耗大?
明顯 a+b 消耗更小。
const?a?=?1;
const?b?=?2;
const?c?=?a?+?b;
這筆賬大家可以自己算,我建議簡(jiǎn)單的基礎(chǔ)類(lèi)型計(jì)算,就不要用 useMemo 了~
5. useState 的正確使用姿勢(shì)
useState 應(yīng)該算最簡(jiǎn)單的一個(gè) Hooks,但在使用中,也有很多技巧可循,如果嚴(yán)格按照以下幾點(diǎn),代碼可維護(hù)性直接翻倍。
5.1 能用其他狀態(tài)計(jì)算出來(lái)就不用單獨(dú)聲明狀態(tài)
一個(gè) state 必須不能通過(guò)其它 state/props 直接計(jì)算出來(lái),否則就不用定義 state。
const?SomeComponent?=?(props)?=>?{
??const?[source,?setSource]?=?useState([
??????{type:?'done',?value:?1},
??????{type:?'doing',?value:?2},
??])
??const?[doneSource,?setDoneSource]?=?useState([])
??const?[doingSource,?setDoingSource]?=?useState([])
??useEffect(()?=>?{
????setDoingSource(source.filter(item?=>?item.type?===?'doing'))
????setDoneSource(source.filter(item?=>?item.type?===?'done'))
??},?[source])
??return?(
????<div>
???????.....
????div>
??)
}
上面的示例中,變量 doneSource 和 doingSource 可以通過(guò)變量 source 計(jì)算出來(lái),那就不要定義 doneSource 和 doingSource 了!
const?SomeComponent?=?(props)?=>?{
??const?[source,?setSource]?=?useState([
??????{type:?'done',?value:?1},
??????{type:?'doing',?value:?2},
????])
??const?doneSource?=?useMemo(()=>?source.filter(item?=>?item.type?===?'done'),?[source]);
??const?doingSource?=?useMemo(()=>?source.filter(item?=>?item.type?===?'doing'),?[source]);
??return?(
????<div>
???????.....
????div>
??)
}
一般在項(xiàng)目中此類(lèi)問(wèn)題都比較隱晦,層層傳遞,在 Code Review 中很難一眼看出。如果能把變量定義清楚,那事情就成功了一半。
5.2 保證數(shù)據(jù)源唯一
在項(xiàng)目中同一個(gè)數(shù)據(jù),保證只存儲(chǔ)在一個(gè)地方。
不要既存在 redux 中,又在組件中定義了一個(gè) state 存儲(chǔ)。
不要既存在父級(jí)組件中,又在當(dāng)前組件中定義了一個(gè) state 存儲(chǔ)。
不要既存在 url query 中,又在組件中定義了一個(gè) state 存儲(chǔ)。
function?SearchBox({?data?})?{
??const?[searchKey,?setSearchKey]?=?useState(getQuery('key'));
??const?handleSearchChange?=?e?=>?{
????const?key?=?e.target.value;
????setSearchKey(key);
????history.push(`/movie-list?key=${key}`);
??}
??return?(
??????<input
????????value={searchKey}
????????placeholder="Search..."
????????onChange={handleSearchChange}
??????/>
??);
}
在上面的示例中,searchKey 存儲(chǔ)在兩個(gè)地方,既在 url query 上,又定義了一個(gè) state。完全可以?xún)?yōu)化成下面這樣:
function?SearchBox({?data?})?{
??const?searchKey?=?parse(localtion.search)?.key;
??const?handleSearchChange?=?e?=>?{
????const?key?=?e.target.value;
????history.push(`/movie-list?key=${key}`);
??}
??return?(
??????<input
????????value={searchKey}
????????placeholder="Search..."
????????onChange={handleSearchChange}
??????/>
??);
}
在實(shí)際項(xiàng)目開(kāi)發(fā)中,此類(lèi)問(wèn)題也是比較隱晦,編碼時(shí)應(yīng)注意。
5.3 useState 適當(dāng)合并
項(xiàng)目中有木有寫(xiě)過(guò)這樣的代碼:
const?[firstName,?setFirstName]?=?useState();
const?[lastName,?setLastName]?=?useState();
const?[school,?setSchool]?=?useState();
const?[age,?setAge]?=?useState();
const?[address,?setAddress]?=?useState();
const?[weather,?setWeather]?=?useState();
const?[room,?setRoom]?=?useState();
反正我最開(kāi)始是寫(xiě)過(guò),useState 拆分過(guò)細(xì),導(dǎo)致代碼中一大片 useState。
我建議,同樣含義的變量可以合并成一個(gè) state,代碼可讀性會(huì)提升很多:
const?[userInfo,?setUserInfo]?=?useState({
??firstName,
??lastName,
??school,
??age,
??address
});
const?[weather,?setWeather]?=?useState();
const?[room,?setRoom]?=?useState();
當(dāng)然這種方式我們?cè)谧兏兞繒r(shí),一定不要忘記帶上老的字段,比如我們只想修改 firstName:
setUserInfo(s=>?({
??...s,
??fristName,
}))
其實(shí)如果是 React Class 組件,state 是會(huì)自動(dòng)合并的:
this.setState({
??firstName
})
在 Hooks 中,可以有這種用法嗎?其實(shí)是可以的,我們自己封裝一個(gè) Hooks 就可以,比如 ahooks 的 useSetState[3],就封裝了類(lèi)似的邏輯:
const?[userInfo,?setUserInfo]?=?useSetState({
??firstName,
??lastName,
??school,
??age,
??address
});
//?自動(dòng)合并
setUserInfo({
??firstName
})
我自己在項(xiàng)目中大量使用了 useSetState 來(lái)代替 useState,來(lái)管理復(fù)雜類(lèi)型的 state,媽媽更愛(ài)我了。
6、總結(jié)
作為資深的 React Hooks 用戶(hù),我很認(rèn)可 React Hooks 帶來(lái)的提效,這也是我這幾年完全擁抱 Hooks 的原因。同時(shí)我也越來(lái)越覺(jué)得 React Hooks 難駕馭,尤其隨著 React 18 的 concurrent mode 的到來(lái),不知道會(huì)帶來(lái)什么坑。
最后再給大家三個(gè)建議:
可以多使用別人封裝好的高級(jí) Hooks 來(lái)提效,比如 ahooks[4] 庫(kù)(哈哈哈 可以多看看別人封裝好的 Hooks 源碼,加深對(duì) React Hooks 理解,比如 ahooks[5] 庫(kù)(哈哈哈 對(duì) hooks 感興趣,可以關(guān)注公眾號(hào):前端技術(shù)磚家
參考資料
ahooks: https://github.com/alibaba/hooks
[2]eslint-plugin-react-hooks: https://www.npmjs.com/package/eslint-plugin-react-hooks#installation
[3]useSetState: https://ahooks.js.org/zh-CN/hooks/use-set-state
[4]ahooks: https://github.com/alibaba/hooks
[5]ahooks: https://github.com/alibaba/hooks

