前言
React Hooks 提供了兩個管理狀態的 hook,分別是 useState
跟 useReducer
,你在寫 component 應該經常會想「什麼時候該用 useState
,又在什麼時候才用 useReducer
」,「 useReducer
跟 redux 這麼像,幹嘛不用 redux 就好」,「沒事 React 團隊幹嘛抄一個這麼像 redux 的東西呢 😆」。
實際上有許多情況直接用 useState
會比要建一個 useReducer
更簡單,因為 useReducer
的使用方法是希望我們用像 redux 的方式來管理狀態,透過 action -> reducer -> store
的方式管理狀態流。你應該不會想在一個簡單的 component 寫一堆 reducer 的程式碼,不然後續維護時看到你可能會想殺掉以前的自己。
但是有些情況 useReducer
會比 useState
更好用,會寫出更容易維護的程式碼,接下來就讓我們來評估 useReducer
跟 useState
兩者的使用時機吧!
👉 一個使用個案
有一個情境如下,現在你需要設計一個書籍清單列表,這個列表除了顯示各種書籍以外,還提供了以下幾個功能:
- 使用者可以切換頁面,瀏覽書籍列表
- 使用者可以自訂每一頁顯示幾本書籍資料
- 使用者可以透過關鍵字搜尋書籍書名或分類
- 使用者可以選擇書籍分類
- 使用者可以用價格排序
根據以上個需求,我們會設計一個物件來處理整件事情,這個物件大概會長的像下面這個樣子:
另外,我們希望這個網頁應用是 SPA (Single Page Application) ,亦即在切換頁面、輸入關鍵字或選擇書籍分類後,網頁不用重新整理就可以從伺服器拿到資料,現在有很多現成的套件可以幫我們做到這件事,像是 react-query
、 swr
等等。
但是我們沒有要在這個網頁中用到這些套件,只是有一個模擬的 useQuery
如下:
👉 useState
的作法
完整的 useState 版本實作 ⏳ Codesandbox
首先,我們先看到 useState
的作法,你大概會想要用幾個 state 來儲存這些參數,看起來很棒,各個狀態一目瞭然。
接下來你會開始撰寫各個事件被觸發時相對應的 callback function,每個 callback 各司其職,在 <Select />
或 <Input />
更新時幫我們把 state 設定回去。最後 useEffect
可以幫我們在關鍵字、分類、頁籤、排序等等的設定改變後把它們都組合起來,儲存在 variables
中。
而 useQuery
會在 variables
改變後,自動重新呼叫 API,跟伺服器請求新的資料。
如果你看完有些疑惑為什麼要這樣寫,像是 handleChangeKeyword
與 handleChangeCategory
之所以會需要重新設定pagination
是因為在輸入關鍵字或是改變書籍類別後,為了瀏覽搜尋結果必須回到第一頁,我們才要設定 current
為 1。
這樣看下來,整體看下來還行,那接著我們繼續來看看 useReducer
的版本。
👉 useReducer
的作法
完整的 useReducer 版本實作 ⏳ Codesandbox
現在我們把書籍列表中的邏輯用 useReducer
來改寫,把 variables 相對應的 callback function 分別都變成一種 action.type
,而回傳的值即是 API 所需要的 variables
。
然後再把原本 <App />
裡面的 useState
替換掉,變成用 dispatch
方式修改 variables
的值。
你應該有發現,用 useReducer
修改過後的 component 精簡了許多,由於複雜的 state 邏輯都移動到了 reducer
裡面,我們在閱讀程式碼時就有跡可循,知道 callback 裡要做哪些事情,不用在閱讀程式碼時被各種複雜的 setState
交互弄亂自己的思考邏輯。
而且這篇文章中使用的網頁應用如果再大一些,需要控制的 fitler 數量很多,就要用很多的 useState
跟 callback 來處理複雜的 state 邏輯,為了後續維護,轉而用 useReducer
反而是更好的一種做法。
🎉 另一個 useReducer 的優點
有時候我們會需要把 callback 當作 props 傳遞到 child component 中,如此一來,為了避免 re-render 的問題同常會用 useCallback
記憶起來,但是代表說我們有非常多的 callback 需要綁在 useCallback
裡面,也許這不是件好事。
而 useReducer
的好處之一便是, dispatch
不會隨著 re-render 而重新分配記憶體位置,在作為 props 傳入到 child component 中時也可以不用擔心沒有 useMemo
而造成 re-render 的問題。
雖說 React 官網也有提到傳遞 callback 可以讓等體閱讀起來更明確,像是「管線」一樣,可以知道 child component 可能會做哪些事。但這是一些取捨,使用
dispatch
的方式會讓程式碼看起來更乾淨些,就看團隊是如何溝通了。
結論
這篇文章我們討論了一個實際上很常見的個案,而從這個個案中可以看到 useReducer
的程式碼比 useState
的程式碼更乾淨些。實際上的個案使用到的 filter 肯定會更多,在複雜的網頁應用中,可以見得 state 邏輯一定也會越來越複雜,在這樣的 component 中使用 useReducer
將 state 邏輯抽離出來,程式碼會變得更容易閱讀及維護。
題外話一下,這篇文章的標題是「 既生 useState
何生 useReducer
」,而且 React 的官方網站也提到 useReducer
是 useState
的一種替代性寫法,讓我們以為它們是不同的東西,但是誰知道 useState
的底層其實是用 useReducer
做的呢 😜。
有興趣請看
useState
的 🔗原始碼
分享就到這邊,如果喜歡我的文章可以幫我拍個幾下手,在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。 😃
Reference
- https://stackoverflow.com/questions/54646553/usestate-vs-usereducer
- https://www.robinwieruch.de/react-usereducer-vs-usestate
- https://hswolff.com/blog/why-i-love-usereducer/
- https://rajeshnaroth.medium.com/why-use-reducer-hooks-for-state-management-in-react-c9528f615ddf
- https://zh-hant.reactjs.org/docs/hooks-reference.html#usereducer
- https://zh-hant.reactjs.org/docs/hooks-faq.html#how-to-avoid-passing-callbacks-down