React.js useRef useState

理解 React.js 中 useRef 及 useState 各自的適用情境

陳志澤 2021/07/23 14:35:03
879

一、前言

在使用 Function Component 時,直覺性都會使用 useState 來保存內部狀態,若狀態使用於 useEffect 中就需強迫被依賴 (資料變動才會更新執行),但有時這並不是我們想要的結果,甚至會造成困擾;此時可以想看看是不是該換 useRef 上場了。

 

 

二、簡易判斷

先說明兩者都可在 component 生命週期中保存資訊,且在 re-render 後仍然可被持續保存,但 useRef 建立出來的變數在被改變時並不會觸發 re-render,也就表示當數值變化後並無法即時呈現在畫面中,因此簡單的判斷依據如下:

是否牽涉畫面顯示  ?  useState  :  useRef

 

 

三、實例說明

舉個實際的例子來說明,在比較嚴謹的業務申請流程中一定會有 OTP 單次性密碼出現,此時必定標配一個倒數秒數在畫面中,表示驗證碼效期正在流逝中,而換匯交易的匯率也存在著效期,因此倒數功能其實還滿廣泛的在應用;對於工程師來說實作一個倒數器說來滿簡單,但是看到一樣的東西被寫了好幾份感覺很不舒服,另外還需要特別注意 setInterval 實體的清除時機,避免不小心 re-render 造成多個實體同時運作的窘境,所以還是抽出邏輯建立個 Hook 共用吧!

 

 

以下為自行建立的 useTimer Hook 方法,主要目的在於等待 delay 秒數,而時間到會執行 callback 方法,在倒數的過程中會不斷輸出 remainSecond 給使用端呈現在畫面中;建議讀者先試想程式碼中分別使用 useState 及 useRef 建立變數的考量點,如果互相對調又會造成什麼影響,最後在看看後文的分析,這樣就會有比較深刻的體會。

 

function useTimer (callback, delay) {
  const [remainSecond, setRemainSecond] = useState(0)
  const savedCallback = useRef()
  const savedDelay = useRef()

  // 保存到期回呼方法
  useEffect(() => {
    savedCallback.current = callback
  }, [callback])

  // 建立計數器並執行倒數
  useEffect(() => {
    // 刷新延遲秒數
    savedDelay.current = delay
    setRemainSecond(delay)

    // 每秒執行
    const tick = (id) => {
      // 計算剩餘時間
      if (savedDelay.current > 0) {
        savedDelay.current -= 1
      } else {
        savedDelay.current = 0
      }

      // 更新輸出的剩餘秒數
      setRemainSecond(savedDelay.current)

      // 停止條件
      if (savedDelay.current <= 0) {
        savedCallback.current()
        clearInterval(id)
      }
    }

    if (delay !== null) {
      // 產生計數器
      const id = setInterval(() => tick(id), 1000)

      // 清除計數器 (cleanup)
      return () => clearInterval(id)
    }
  }, [delay])

  // 輸出剩餘秒數
  return remainSecond
}

 

使用 useRef / useState 產生變數的考量點:

  1. 由於 remainSecond 變動時需要即時顯示在畫面中,因此使用 useState 處理。

  2. 由於 callback 僅在時間到期時被叫用,所以筆者不希望 callback 變動時會造成計數器的重新建立,因此不能在產生計數器的 useEffect 中被直接依賴使用,故透過 useRef 建立 savedCallback 來存放,並利用依賴 callback 的 useEffect 方法更新 callback 方法,這樣就可於建立計數器的 useEffect 中使用 savedCallback.current() 且不被依賴喔!

  3. 至於 delay 數值需要在組件中不斷遞減,所以需要先被以 savedDelay 暫存起來才能修改,此時若使用 useState 建立狀態,只要在建立計數器的 useEffect 中操作遞減行為時會被依賴,只要遞減就重新建立一個新的計數器,這不是我們要的結果,因此使用 useRef 來建立 savedDelay 變數,這樣就可於建立計數器的 useEffect 中使用 savedDelay.current 遞減秒數且不被依賴喔!

 

 

四、實例驗證

最後驗證一下我們的代碼邏輯是否正確,寫個測試 useTimer 的 Hook 範例如下。

 

可執行倒數並輸出剩餘秒數於畫面上,並且當倒數秒數 delay 變化時會重新開始計數。

 

 

 

陳志澤