2026/04/06

最近把專案升級到 Next.js 16(搭配 React 19)之後,最明顯的感受就是 ESLint 變得非常有「正義感」。很多以前我們習慣「隨手寫一個 useEffect 來同步狀態」的寫法,現在都會被編譯器盯得滿頭包。(詳見此 lint)
其實這是一件好事。React 團隊一直強調:如果你可以透過計算得出結果,就不要用 useEffect。 今天就拿我剛重構完的 AppWrapper 為例,聊聊怎麼拿掉那些不必要的 useEffect。
useEffect 該被殺掉?先看看我原本的 AppWrapper 邏輯:
1// ❌ 舊的寫法:多此一舉的同步
2const hasUpdate = useCheckVersion(); // 假設這是一個回傳 boolean 的 Hook
3const [showUpdateDialog, setShowUpdateDialog] = useState(false);
4
5useEffect(() => {
6 if (hasUpdate) {
7 setShowUpdateDialog(true);
8 }
9}, [hasUpdate]);這段程式碼的意圖很簡單:當發現有新版本(hasUpdate 為 true)時,就把彈窗打開。但仔細想想,我們真的需要 useState 加 useEffect 嗎?
在 React 的渲染流程中,這種寫法會導致:
hasUpdate 變為 true,但 showUpdateDialog 還是 false。setShowUpdateDialog(true)。這就是標準的「二度渲染」,既浪費效能又讓程式碼變得難以維護。
在 React 19 的環境下,我們應該追求的是單一真相來源(Single Source of Truth)。
既然 hasUpdate 本身就是一個已經算好的狀態,我們根本不需要再開一個 useState 去存它,直接拿來用就好了:
1// ✅ 最佳化後的寫法:直接衍生
2const hasUpdate = useCheckVersion();
3
4// 如果不需要使用者手動關閉,或是它只是一個唯讀的狀態,直接傳進去就好
5return (
6 <>
7 {children}
8 <NewVersionAlertDialog open={hasUpdate} />
9 {/* ... 其他元件 */}
10 </>
11);有的時候,我們希望版本更新通知跳出來後,使用者可以按「稍後再說」來關閉。這時候才需要 useState,但邏輯要倒過來想:
1const hasUpdate = useCheckVersion();
2const [isDismissed, setIsDismissed] = useState(false);
3
4// 只有在「有更新」且「使用者還沒忽略」時才顯示
5const shouldShowDialog = hasUpdate && !isDismissed;
6
7return (
8 <NewVersionAlertDialog
9 open={shouldShowDialog}
10 onClose={() => setIsDismissed(true)}
11 />
12);這樣寫,我們依然成功拿掉了 useEffect。
升級到 Next.js 16 / React 19 後,強烈建議大家重新檢視元件內的 useEffect。
「少寫一個 useEffect,就少一個 Bug。」
這不只是為了通過 ESLint 的檢查,更是為了讓程式碼更符合 React 的宣告式(Declarative)精神。高品質的程式碼不一定要很複雜,通常「簡單」才是最難維持的。