React usePrevious 系列:從 useRef 到 useDeferredValue
問題:如何拿到上一次的值?
在 React 開發中,「比較前後兩次 render 的值」是一個常見需求。比如動畫(需要知道從哪個值變到哪個值)、效能優化(只在特定 prop 變化時執行邏輯)、或偵測變化方向。
最經典的做法是 usePrevious hook,但實作方式比你想像的有更多眉角。
方式一:useRef + useEffect
這是最廣為流傳的版本:
unknown node原理很簡單:useEffect 在 render 之後才執行,所以在 render 過程中讀到的 ref.current 還是上一次的值。等 render 結束,effect 才把新值存進去。
問題:這個版本在 Strict Mode 下有微妙的行為——React 18 的 Strict Mode 會 double-invoke effect,導致 ref 被更新兩次。雖然在 production 不影響,但開發時可能造成困惑。
方式二:useRef 直接同步更新
既然 useEffect 有時機問題,何不直接在 render 階段更新?
unknown node這個版本透過兩個 ref 來追蹤——一個存「現在的」,一個存「上一次的」。當偵測到值改變時,把舊的搬到 prev,新的存進 curr。
問題:在 render 階段寫入 ref 可能在 Concurrent Mode 下造成問題,因為 render 可能被中斷重來。
方式三:useDistinctPrevious
有時候我們想要的不是「上一次 render 的值」,而是「上一次不同的值」。
unknown node差別在於加入了 isEqual 比較函式。這在處理物件或陣列時特別有用——即使 reference 不同但內容相同,我們可能不想更新 previous 值。
方式四:useDeferredValue 的巧妙應用
React 18 的 useDeferredValue 原本是用於效能優化的——它會延遲更新非緊急的值。但有人發現它可以間接實現 usePrevious 的效果:
嚴格來說這不是 usePrevious,但概念類似——你拿到了一個「稍微落後」的值,可以用來做過渡效果。
該用哪個?
- 大部分情況:方式一(useRef + useEffect)就夠了
- 需要精確比較:方式三(useDistinctPrevious)
- 做過渡動畫:考慮 useDeferredValue
- 避免在 render 階段寫入 ref,除非你清楚 Concurrent Mode 的影響