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