React18 的 useEffect 新特性為什么被瘋狂吐槽?
react18 已經(jīng)出來一段時間了,create-react-app 默認安裝的 React 版本也已經(jīng)是 18+,不知道有沒有小伙伴發(fā)現(xiàn)自己有點看不懂 React 了?
import?{?useEffect,?useState?}?from?'react'
function?App?()?{
??const?[data,?setData]?=?useState(0)
??
??useEffect(()?=>?{
????setData(preData?=>?preData?+?1)
??},?[])
??
??return?(
????<div>{data}div>
??)
}
看一下這段簡單的代碼,頁面最終展示的數(shù)字是幾?
是 1 這樣嗎?我覺得應(yīng)該也是這樣,可事實就是在 React18 里,這并不是預(yù)期效果,最終展示的其實是 2,為什么呢?
useEffect 的"新特性"
根據(jù) React 最新的文檔[1] 中對于 useEffect 的介紹得知,之所以我們剛才的例子最終展示的是 2 而不是 1 的原因是,在 dev 環(huán)境下,React 會將每個組件掛載兩次進行測試。測試什么?測試你的 useEffect 有沒有潛在問題
大家都知道函數(shù)式組件掛載后,會執(zhí)行 useEffect 定義的副作用;在組件卸載時,會執(zhí)行 useEffect return 出來的回調(diào)執(zhí)行一些組件卸載時的行為,即:
function?App?()?{
??useEffect(()?=>?{
????console.log('組件掛載了')
????return?()?=>?{
??????console.log('組件卸載了')
????}
??},?[])
??
??return?(
????<div>useEffectdiv>
??)
}
從組件掛載到卸載就會依次打?。?/p>
組件掛載了
組件卸載了
而在 React18 里,是這樣打印的:
組件掛載了
組件卸載了
組件掛載了
按照文檔里所說的,之所有這么做的,是為了通過掛載兩次組件來提早發(fā)現(xiàn)你的問題,例如:
import?{?useEffect,?useState?}?from?'react'
function?App?()?{
??const?[data,?setData]?=?useState(0)
??
??useEffect(()?=>?{
????setInterval(()?=>?{
??????setData(preData?=>?preData?+?1)
????},?1000)
??},?[])
??
??return?(
????<div>{data}div>
??)
}
這段代碼時很多剛使用 React 的同學(xué)經(jīng)常會犯的錯誤,在 useEffect 里定義了個定時器,但沒有在任何地方去清除它,所以即使在組件卸載了,這個定時器仍然還在運作,不光造成了內(nèi)存泄漏,還可能會導(dǎo)致程序出現(xiàn)問題
所以就基于這段錯誤的代碼,React18 執(zhí)行 掛載 => 卸載 => 掛載,你就會發(fā)現(xiàn),實際是有兩個定時器在跑的,所以原本你想每秒 data + 1,變成了每秒 data + 2,如此明顯的問題一下就被我們發(fā)現(xiàn)了
那正確的做法就是在 useEffect 里 return 一個用于卸載時執(zhí)行的回調(diào)函數(shù):
import?{?useEffect,?useState?}?from?'react'
function?App?()?{
??const?[data,?setData]?=?useState(0)
??
??useEffect(()?=>?{
+???const?timer?=?setInterval(()?=>?{
??????setData(preData?=>?preData?+?1)
????},?1000)
?
+???return?()?=>?clearInterval(timer)
??},?[])
??
??return?(
????{data}
??)
}
這樣就沒有問題了。謝謝 React18 這個"獨特"的新特性(手動狗頭)
單單基于這個出發(fā)點,我覺得是非常好的,能幫我們提早發(fā)現(xiàn)問題,解決問題,而不是等發(fā)到線上后造成了性能問題,回過頭來再逐一排查。而且這只會在開發(fā)環(huán)境才會掛載兩次,生產(chǎn)環(huán)境還是正常的
但真的是個完美的特性嗎?根據(jù)網(wǎng)友的吐槽和我目前使用下來的感受,給我們造成的麻煩可能大于它本身的好處了
即使我的 useEffect 里根本沒有需要在卸載時清理的對象,它也會被執(zhí)行兩次,比如請求兩次、賦值兩次 ... 這似乎是給我們造成了不少的負擔(dān)啊,不知道的以為是別的地方出了 bug 呢!
關(guān)閉特性
我也可以手動關(guān)閉這個特性,找到入口文件 main.tsx,把 StrictMode 標簽給去掉就好了
mport?React?from?'react'
import?ReactDOM?from?'react-dom/client'
import?App?from?'./App'
import?'./index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
-?
????
-?
)
不過這樣也把其它的提示給一并干掉了,其實我是不想這么做的
只是這樣?
有很多人都在吐槽著!比如:初始化時 useEffect 會造成兩次請求的話,似乎我們也不該在 useEffect 中發(fā)起請求?

然而 Dan 給出的解釋就是說,你應(yīng)該在服務(wù)端渲染時就請求到數(shù)據(jù),而不是在客戶端渲染掛載了 DOM 后才請求數(shù)據(jù)
其實 React18 將在之后推出一些別的功能,這個模擬組件重新掛載的特性只是為之后的功能做準備的,具體是什么功能呢?類似于 Vue 的 KeepAlive[2]
最后
簡單總結(jié)一下:這個特性出發(fā)點是好的,同時也是為了之后的新特性做準備。但推出這個功能的同時也要考慮一下開發(fā)者的體驗(起碼是大部人的開發(fā)體驗),不然真的是得不償失。
對于 useEffect 這個新特性,你怎么看?歡迎在評論區(qū)留言!
參考資料
React 最新的文檔: https://beta-reactjs-org-git-effects-fbopensource.vercel.app/learn/synchronizing-with-effects
[2]KeepAlive: https://vuejs.org/guide/built-ins/keep-alive.html#basic-usage
