這個(gè)鉤子可能會(huì)讓你的代碼變得更糟
前陣子,我在開(kāi)發(fā)一個(gè)功能爆炸的后臺(tái)儀表盤(pán)。
那種看上去清清爽爽、實(shí)則背后隱藏了 47 個(gè) API 請(qǐng)求和 5 個(gè) loading 狀態(tài)的“地獄級(jí)組件”。
理所當(dāng)然地,我滿腦子就是:這不就用 useEffect
嗎?
- 要獲取數(shù)據(jù)?
useEffect
- 要加事件監(jiān)聽(tīng)?
useEffect
- 要把 prop 同步到本地狀態(tài)?你猜對(duì)了,還是
useEffect
然后,我的組件代碼長(zhǎng)這樣:
useEffect(() => { /* 獲取數(shù)據(jù) */ }, [query])
useEffect(() => { /* 同步 prop 到 state */ }, [someProp])
useEffect(() => { /* 加事件監(jiān)聽(tīng) */ }, [])
// 再來(lái)一個(gè),監(jiān)聽(tīng) state 更新另一個(gè) state...
每一個(gè) useEffect
看起來(lái)都“有理有據(jù)”,直到——
bug 開(kāi)始源源不斷地冒出來(lái)。
- 副作用多次執(zhí)行
- 內(nèi)存泄漏
- 幽靈請(qǐng)求
- 還有最致命的:一個(gè) state 的更新觸發(fā)了 effect,結(jié)果 effect 又更新了 state,直接進(jìn)了無(wú)限循環(huán)地獄
那一刻,我開(kāi)始懷疑人生:
我是在正確使用 useEffect,還是只是在用它修補(bǔ)糟糕的狀態(tài)管理?
問(wèn)題不是 useEffect,而是你用它的方式
useEffect
本身并沒(méi)錯(cuò),功能強(qiáng)大、靈活性高。
但也正因如此,它屬于React 中“底層”的能力。
每寫(xiě)一個(gè) useEffect
,你其實(shí)是在告訴 React:
“嘿,render 后我要干點(diǎn)別的事,可能會(huì)改 state、改 DOM、改外部系統(tǒng)。”
如果你不熟悉組件生命周期、不理解依賴(lài)數(shù)組、不清楚副作用的運(yùn)行時(shí)機(jī),你的代碼就可能失控。
我曾經(jīng)踩過(guò)的坑包括:
- 在組件里直接 fetch 數(shù)據(jù),而不是集中在數(shù)據(jù)層
- 不必要地將 props 同步到 state
- 用
useEffect
來(lái)“派生狀態(tài)”,而不是直接在 render 里計(jì)算 - 狀態(tài)改變導(dǎo)致重新渲染 → 又觸發(fā) effect → 又更新?tīng)顟B(tài) → 死循環(huán)
后來(lái)我這樣“重構(gòu)”了思維方式
我開(kāi)始問(wèn)自己 3 個(gè)問(wèn)題:
- 這個(gè)邏輯必須要放在
useEffect
里嗎? - 能不能直接在 render 里表達(dá)?
- 這個(gè)狀態(tài)應(yīng)該放在這里,還是該“上提”或“下沉”?
下面是我真正改變代碼風(fēng)格的幾個(gè)關(guān)鍵做法。
1. 狀態(tài)派生,不要復(fù)制
以前我會(huì)寫(xiě):
useEffect(() => {
setIsMobile(window.innerWidth < 768)
}, [])
現(xiàn)在我改為:
const isMobile = useMediaQuery('(max-width: 768px)')
甚至有時(shí)候,直接用 CSS 做響應(yīng)式布局更合理。
狀態(tài)派生邏輯單獨(dú)封裝在 hook 里,讓組件更干凈。
2. 不再同步 prop 到 state
以前我習(xí)慣這樣寫(xiě):
useEffect(() => {
setValue(propValue)
}, [propValue])
現(xiàn)在我直接:
const value = propValue
或者,如果需要做變換,就:
const value = useMemo(() => transform(propValue), [propValue])
完全不需要多此一舉地復(fù)制值。
3. 數(shù)據(jù)獲取放到組件外
以前我在組件中發(fā)請(qǐng)求:
useEffect(() => {
fetch(...)
}, [query])
現(xiàn)在我用更合適的工具,比如:
SWR
React Query
- Next.js 的 Server Actions
示例(SWR):
const { data, isLoading } = useSWR(`/api/data?id=${id}`, fetcher)
不用再自己維護(hù) loading、error、緩存邏輯,代碼自然也更穩(wěn)定。
4. 拆出自定義 Hook
如果副作用邏輯復(fù)雜,不要塞進(jìn)組件內(nèi)部,而是封裝成一個(gè) hook:
function useScrollToTop() {
useEffect(() => {
window.scrollTo(0, 0)
}, [])
}
然后在組件中這樣用:
useScrollToTop()
結(jié)構(gòu)更清晰,可維護(hù)性也高。
5. 事件監(jiān)聽(tīng)也要“聲明式”
以前的寫(xiě)法:
useEffect(() => {
window.addEventListener('resize', handler)
return () => window.removeEventListener('resize', handler)
}, [])
現(xiàn)在我用社區(qū)現(xiàn)成的 useEventListener
hook,或者自己封裝。
useEventListener('resize', handler)
更干凈,也更容易看懂。
寫(xiě)在最后:不是別用 useEffect,而是別亂用
我不是說(shuō) useEffect
不該用,它依然是以下場(chǎng)景的剛需:
- 訂閱 / 取消訂閱
- 設(shè)置 / 清理定時(shí)器
- 操作 DOM
- 處理外部副作用(socket、analytics 等)
但真正的關(guān)鍵在于:
你是否真的需要副作用?還是只是用它掩蓋結(jié)構(gòu)混亂?
React 本身是響應(yīng)式的。
當(dāng) props 或 state 變化時(shí),組件會(huì)自動(dòng)重新渲染。很多邏輯本就可以放在 render 中表達(dá),不需要“繞一圈”寫(xiě)副作用。
每次想寫(xiě) useEffect
之前,請(qǐng)先問(wèn)自己:
- 這個(gè)邏輯可以放在 render 函數(shù)里嗎?
- 能不能用 hook 封裝一下?
- 它是真正的副作用,還是我寫(xiě)錯(cuò)了狀態(tài)結(jié)構(gòu)?
- 有沒(méi)有更聲明式的方式表達(dá)這個(gè)意圖?
有時(shí)候,最干凈的 React 代碼,是最“無(wú)聊”的那種:
- 沒(méi)有奇怪的 useEffect
- 沒(méi)有同步 props 到 state 的騷操作
- 就只是 props → state → render 的閉環(huán)
??♂? 代碼越穩(wěn),Bug 越少。副作用少寫(xiě)一點(diǎn),自己也會(huì)少掉幾根頭發(fā)。