React
useEffect
hooks
本篇為 [FE302] React 基礎 - hooks 版本 這門課程的學習筆記。如有錯誤歡迎指正!
在 React Hooks 當中,最重要的就是 useState 和 useEffect,若能學會如何使用這兩個 hook,對於 React 應用也會更容易上手。
詳細可參考官方文件:使用 Effect Hook。
簡單來說,就是透過 useEffect 這個 hook,告訴 React「component 在 render 之後要做的事情」。
有別於一般的 hook 是傳值進去,userEffect 傳入的是 function,使用方法如下:
就會在每次畫面 render 結束後執行 useEffect 傳入的 function:
但通常我們不會想要在每次 render 後都執行 function,像是設定在某些 state 改變時才會執行。
以把 todo APP 同步到 LocalStorage 這個功能為例:
此外,還有很重要的一點,就是之前實作的 setTodos() 功能其實是非同步行為。
如果在新增 todo 的同時進行 console.log(todos)
,會發現畫面 render 了,todos 卻還沒有更新:
因此不能直接在 function 中寫入 todos,而是要直接寫入更新過的狀態,其他功能也以此類推,在每次改變 todo 時都要執行 writeTodosToLocalStorage():
上述這種做法,其實是我們過去利用 jQury 實作的想法,在變動資料的同時進行其他動作。
但其實進行新增、編輯、刪除 todo 時有個共通點,就是會「todos 會改變」,接著就是 useEffect 登場的時候了!
因為 useEffect() 會在每次 render 後執行,有 render 就代表 state 有變動。一旦有變動就執行同步 function,可把程式碼改寫如下:
這樣就成功在每次 render 後,都把最新的 todos 狀態同步到 localStorage:
但這樣做其實有個缺點,透過執行的 console.log(),可發現連在輸入 input 時也會執行 render,應該只需要在 todos 有改變時才進行 render。
而 useEffect 的第二個參數可以解決這個問題,需傳入一個陣列,用來放想要關注的資料,當變數改變時才會執行 useEffect:
可改寫如下,代表在 todos 改變時才會重新執行 useEffect():
透過 localStorage 的記憶功能,我們就能在頁面第一次 render 結束後,把 localStorage 中的 todos 同步到頁面上。
在第二個參數傳入空陣列,就只有第一次 render 會執行這個 useEffect,可用來進行初始化:
但是在重整頁面瞬間,會發現畫面閃了一下,這是因為第一次 render 畫面顯示的是 useState 初始設定,第二次 render 才是放入 todoDate:
那麼該如何解決 useEffect 這個問題呢?接下來會繼續介紹其他功能來改善。
我們在開頭提到,可透過 useEffect 這個 hook,告訴 React「component 在 render 之後要做的事情」。
但其實更精確的,應該是「在 render 完,瀏覽器 paint 以後要做的事情」,所以才會有 render 後畫面閃一下的情況發生。
而 useLayoutEffect 這個 hook,則是「在 render 完,瀏覽器 paint 以前要做的事情」。
也就是說,和 useEffect 功能其實很類似,差別在於同步與非同步:
實際修改剛才的程式碼:
如此畫面就不會再閃一次初始的資料了:
至於為什麼會產生這個情況,可從 React 的 Hook Flow 談起。
(圖片來源:https://github.com/donavon/hook-flow/blob/master/README.md)
Hook 執行流程可分為三個部分:
原本是在瀏覽器 paint 之後才 run effects,若能提早改變 state 並更新畫面,就會直接顯示最新的 state,而不會出現初始 state。
除了透過 useLayoutEffect,還有另一種做法,同樣能解決畫面閃一下的問題,也就是接下來要介紹的 lazy initializer。
因為 useState 可以傳入初始值,那就直接把要更新的 todoDate 作為 state 初始值:
但這麼會產生另一個問題,就是只有第一次 render 才會執行 useState 初始值,但後續 render 還是會進行撈取 todoData 的動作,又因為 useState 已經有值了,React 就會忽略裡面的東西,這其實會造成效能上的浪費。
useState 除了設定初始值,其實可以傳入一個 function,經由 function return 的值就會是 state 的初始值:
又因為初始值改變了,也要重新設定 todo id,修改後如下:
- JSON.stringify():將資料轉為 JSON 格式的字串
- JSON.parse():將資料由 JSON 格式字串轉回原本的資料型別
像這樣在 useState 透過傳入 function 來設定初始值,就是 run lazy initializer 的過程。因為只有第一次會執行,適合用於一些複雜的運算,這樣 function 就只會被執行一次,避免每次 render 產生的效能問題。
在 Hook Flow 中,有個步驟其實是先 cleanup effect,然後再 run effect,這是什麼意思呢?
繼續用剛才的 todos 為範例,以下程式碼代表「每當 todos 改變,就會執行 useEffect 中的 function」:
但其實在這個 function 中可以 return 另一個 function,又稱為 cleanup function,代表「在這個 effect 被清掉之前要做的事情」:
每次畫面渲染時,其實就是執行一次 APP() 這個 function,可透過這段程式碼來模擬流程:
結合上述範例,cleanup function 執行的時間點有兩個:
那我們可以透過 useEffect 的 cleanup function 做什麼呢?例如:
以下方範例來說,代表「只有在這個 component 被 unmount 會執行 cleanup function」,又因為第二個參數是空陣列,所以這個 useEffect 只會執行一次:
接下來要談談 hooks 最強大的地方,就是我們其實能寫一個自己 hook,又稱作 custom hook,命名開頭必須是 use 開頭,詳細內容可參考官方文件。
以 input 元素為例,我們可以把 value 和 handleInputChange 等行為包在 useInput.js 檔案,寫法和之前的 APP.js 很類似:
就可以用從 useInput.js 讀取到的 handleChange,取代原本的 handleInputChange:
修改完程式也能正常運行,這樣寫的好處就是,如果有第二個 input 時,也能使用共通的邏輯,例如:
我們也可以把 todos 的邏輯獨立成一個 hook,也就是 useTodos.js:
並引入 APP.js 使用:
若再繼續細分功能,甚至可以做到把 UI 和 todos 邏輯完全分開,改寫如下:
其實和之前寫前後端分離的時候很類似,寫成自訂 hook 的過程,就像是把不同邏輯的 function 給模組化,這麼說似乎也沒錯,畢竟 hook 就是 fucntion。
透過抽出共同邏輯的方式,可將功能包裝在 hooks,就算是在不同 UI,也同樣能利用 return 的值,在畫面上呈現想要的資料。
hooks 基本上可以分成下列幾種:
推薦閱讀 Dan Abramov 所撰寫有關 React 的系列文章,裡面對於 useEffect 的原理有更詳細敘述:
第 11 屆 iT 邦幫忙鐵人賽有關 React 的系列文章:
參考資料: