2026/04/06

延續上一篇關於優化 useEffect 的話題,React 19 的新 ESLint 規則(主要是 set-state-in-effect)雖然嚴格,但也推動我們去思考更「React」的解法。
今天我想聊另一個很常見的場景:「當 Props 改變時,我要重置組件內的某些狀態(useState)。」
過去我們幾乎直覺式地會寫出一個 useEffect 來監聽特定 Prop,然後在裡面 setState(initialValue)。但其實在很多時候,我們可以利用 React 內建的機制 —— Key —— 來優化這個流程,完全拿掉這個 useEffect。
今天就拿我手邊這個用於顯示截斷文字與 Modal 的 ContentModal 元件當例子。
useEffect 看起來很礙眼?先來看看原版的程式碼片段:
1//------------------------------- 元件主體 --------------------------------
2export default function ContentModal({
3 content,
4 maxLines = 3,
5 // ... 其他 props
6}: ContentModalProps) {
7 //------------------------------- 狀態管理 --------------------------------
8 // 當文字太長時,使用者可以點擊「更多」來打開 Modal 顯示全文
9 const [modalOpen, setModalOpen] = useState(false);
10
11 // ...中間的文字處理邏輯 (processContentLineBreaks)
12
13 //------------------------------- 副作用處理 --------------------------------
14 // ❌ 舊的寫法:利用 useEffect 監聽 content 改變來重置狀態
15 // 意圖:當這張卡片的文字變了,舊的「展開更多」Modal 應該要關閉
16 useEffect(() => {
17 setModalOpen(false);
18 }, [content]);
19
20 // ... 渲染邏輯
21}這段程式碼的功能完全沒問題。當 content 屬性改變(例如使用者切換到不同的活動卡片)時,我們希望 modalOpen狀態被重置回 false,確保使用者不會看到一個舊內容的彈窗。
但這樣的寫法依然有我們上一篇提到的問題:「二度渲染」。
modalOpen 仍是舊的 true。useEffect。useEffect 執行 setModalOpen(false),觸發二度渲染。雖然在視覺上這可能只是一瞬間,但在 React 19 的新 ESLint 規則看來,這就是一個需要被優化的同步副作用。
key與其在子元件(Child)裡面辛苦地監聽 Props 變動,我們應該把這個重置的責任交給父元件(Parent)。
在父元件渲染 ContentModal 的時候,我們可以利用 React 的 key 屬性。
Jacky 的 React 複習小筆記: 在 React 中,key不只是用在 List 渲染。它更是告訴 React 「這是一個全新的元件實體(Instance)」 的關鍵。當 React 發現元件的key從A變成了B,它不會去進行複雜的差異比對(Diffing),而是直接卸載(Unmount)舊的元件,然後掛載(Mount)一個全新的元件。
利用這個特性,我們可以把重置的職責倒過來想:
直接殺掉那個礙眼的 useEffect:
1//------------------------------- 組件主體 --------------------------------
2export default function ContentModal({
3 content,
4 maxLines = 3,
5 // ... 其他 props
6}: ContentModalProps) {
7 //------------------------------- 狀態管理 --------------------------------
8 // 此狀態只負責這個實體本身是開還是關
9 const [modalOpen, setModalOpen] = useState(false);
10
11 // ...中間的文字處理邏輯
12
13 // ✅ 殺掉副作用!不需要它了
14 // useEffect(() => {
15 // setModalOpen(false);
16 // }, [content]);
17
18 // ... 渲染邏輯
19}在父元件(例如一個卡片列表)渲染它時,把原本用作依賴的 Prop(這裡就是 content)直接當作 key 傳進去(如果 content 太長,可以使用 id 或其他能唯一識別內容的值):
1// 假設這是父元件
2function ParentComponent({ data }) {
3 // data.content 改變時,key 也跟著變
4 return (
5 <ContentModal
6 key={data.id || data.content} // ✅ 關鍵在此:直接利用 key
7 content={data.content}
8 // ...
9 />
10 );
11}當父元件將 key 從「內容A」改為「內容B」時,React 的行為如下:
key 與內容A不同。ContentModal 實體(擁有 modalOpen=true)被完全卸載。ContentModal 實體被掛載。modalOpen=false(因為 useState(false))。useEffect。在 Next.js 16 / React 19 的新時代,重新審視 useEffect 是一個非常重要的習慣。
下次當你直覺地想寫出一個監聽 Props 來重置 useState 的 useEffect 時,先停下來想一想:「我是不是可以直接把這個 Prop 當作 key 傳給父元件?」
少寫一個 useEffect,就少一個 Bug,還順便優化了效能。這種不用自己手動 useRef + shallow 就得到的效能提升,才是最香的(笑)。