React18 的 useEffect 新特性為什麼被瘋狂吐槽?

作者 | 零一

來源 | 前端印象

react18 已經出來一段時間了,create-react-app 默認安裝的 React 版本也已經是 18+,不知道有沒有小夥伴發現自己有點看不懂 React 了?

import { useEffect, useState } from'react'

functionApp () {
const [data, setData] = useState(0)

useEffect(() => {
setData(preData => preData + 1)
}, [])

return (

{data}

)
}

看一下這段簡單的程式碼,頁面最終展示的數字是幾?

1 這樣嗎?我覺得應該也是這樣,可事實就是在 React18 裡,這並不是預期效果,最終展示的其實是 2,為什麼呢?

useEffect 的”新特性”

根據 React 最新的文件[1] 中對於 useEffect 的介紹得知,之所以我們剛才的例子最終展示的是 2 而不是 1 的原因是,在 dev 環境下,React 會將每個元件掛載兩次進行測試。測試什麼?測試你的 useEffect 有沒有潛在問題

大家都知道函數式元件掛載後,會執行 useEffect 定義的副作用;在元件卸載時,會執行 useEffect return 出來的回調執行一些元件卸載時的行為,即:

function

App

() {
useEffect(() => {
console.log('元件掛載了')
return() => {
console.log('元件卸載了')
}
}, [])

return (

useEffect

)
}

從元件掛載到卸載就會依次列印:

元件掛載了
元件卸載了

而在 React18 裡,是這樣列印的:

元件掛載了
元件卸載了
元件掛載了

按照文件裡所說的,之所有這麼做的,是為了通過掛載兩次元件來提早發現你的問題,例如:

import { useEffect, useState } from'react'

functionApp () {
const [data, setData] = useState(0)

useEffect(() => {
setInterval(() => {
setData(preData => preData + 1)
}, 1000)
}, [])

return (

{data}

)
}

這段程式碼時很多剛使用 React 的同學經常會犯的錯誤,在 useEffect 裡定義了個定時器,但沒有在任何地方去清除它,所以即使在元件卸載了,這個定時器仍然還在運作,不光造成了記憶體洩漏,還可能會導致程序出現問題

所以就基於這段錯誤的程式碼,React18 執行 掛載 => 卸載 => 掛載,你就會發現,實際是有兩個定時器在跑的,所以原本你想每秒 data + 1,變成了每秒 data + 2,如此明顯的問題一下就被我們發現了

那正確的做法就是在 useEffectreturn 一個用於卸載時執行的回呼函式:

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 這個”獨特”的新特性(手動狗頭)

單單基於這個出發點,我覺得是非常好的,能幫我們提早發現問題,解決問題,而不是等發到線上後造成了性能問題,回過頭來再逐一排查。而且這隻會在開發環境才會掛載兩次,生產環境還是正常的

但真的是個完美的特性嗎?根據網友的吐槽和我目前使用下來的感受,給我們造成的麻煩可能大於它本身的好處了

即使我的 useEffect 里根本沒有需要在卸載時清理的對象,它也會被執行兩次,比如請求兩次、賦值兩次 … 這似乎是給我們造成了不少的負擔啊,不知道的以為是別的地方出了 bug 呢!

關閉特性

我也可以手動關閉這個特性,找到入口檔案 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 中發起請求?

然而 Dan 給出的解釋就是說,你應該在服務端渲染時就請求到資料,而不是在客戶端渲染掛載了 DOM 後才請求資料

其實 React18 將在之後推出一些別的功能,這個模擬元件重新掛載的特性只是為之後的功能做準備的,具體是什麼功能呢?類似於 Vue 的 KeepAlive[2]

最後

簡單總結一下:這個特性出發點是好的,同時也是為了之後的新特性做準備。但推出這個功能的同時也要考慮一下開發者的體驗(起碼是大部人的開發體驗),不然真的是得不償失。

對於 useEffect 這個新特性,你怎麼看?歡迎在評論區留言!

參考資料

[1]

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