npm uninstall redux

pxn
9 min readFeb 16, 2019

--

後hooks時代,我把redux從專案中移除了…

早先16.3 推出了Context Api 時,我就有寫過一篇文章:

該不該用context api 來取代 redux?

說明 redux 的角色日漸式微,其實你的 react 專案不一定非得搭配 redux 不可,去年底 react team 宣布了 Hooks Api ,引進各種炫砲新工具,useReducer、useContext、useEffect,種種跡象顯示擺脫 redux 指日可待

「多想兩分鐘,其實你可以不用redux」,這兩天挑了一個小規模的專案重新改寫練練手,結果遠比想像中簡單呢…

改寫之前

先來個前情提要,我改寫的專案使用 redux 情境如下:

  • 專案使用 redux 但極少量使用來做 state management ,多數商業邏輯實踐在 Route level 的元件中
  • 搭配使用 redux-persist 來處理 data persistence,狀態儲存在 localStorage 或 sessionStorage 中
  • 搭配使用 redux-saga 來處理 ajax request
  • 搭配使用 redux-responsive 來處理 responsive design

如果要把 redux 從專案中抽掉,就必須要尋找上述需求的替代方案,我的作戰計畫如下:

  • 使用 useReducer 跟 useContext 來打造一個類似 redux 的輕量 State tree datastore,拿來存 App level 的共用資料
  • 自刻一個 persist enhancer 搭配上述的 reducer 使用
  • 在元件中直接使用 useState,useEffect 來處理 ajax request
  • 自刻一個 useBreakpoints 的 Hooks 來取代 redux-responsive

這邊先假設讀者對 Hooks 都有初步認識,所以接下來的內容會直指問題核心,如果對 Hooks 還不清楚的朋友要煩請先到官網把文件讀個幾遍喔

useReducer + useContext = redux

sort of…

撇開 middleware、enhancer 這些外掛不說,redux 不過是一個 State tree datastore,透過 react-redux 提供的 Provider 元件從最頂層包住整個 App,在需要連結的子(孫)元件使用 connect HOC ,state tree mutate的時候就可以透過 props 接到更新

這個部分 Context Api 完全可以取代

State tree mutation 則是透過 reducer 這樣的 pattern

function reducer (state, action) {
// do something
return newState
}

所謂的 reducer 簡單說,某個 action 透過 dispatch 發佈進到 reducer 裡,你拿 action payload 跟上一動的 state 攪和攪和以後產生一個新 state 然後回傳,這就是 reducer 的真面目

回頭看看 Hooks 官方文件的 useReducer ,我說這使用姿勢相似度87像

redux:抗議!他抄我。
useReducer:這就跟跑步游泳一樣,大家 flux一家親,沒有誰抄誰辣

所以技術上來說,要達成一個輕量化 redux ,使用 useContext + useReducer 是完全可行的,不過實作上有些眉眉角角要注意…

官網提供的 useReducer 範例只是單純使用單一 switch case reducer,而多數人使用 redux 習慣上是把 reducer 分拆成數個方便管理,再用 redux 提供的 combineReducers 合併成一個 rootReducer

我本來想沿用舊代碼,但發現這個 rootReducer 似乎跟 useReducer 接不太上,奇怪,記得明明是同樣的 signature ,有可能 redux 內部還有多做其他的事,後來轉念一想,最終目標是要丟掉 redux 相依,刻一個 combineReducers 也不難,就隨手寫了 helper

此外,早先寫 reducer 的時候我們有採用 reduxsauce 這樣的 helper,用 handlers map 比 switch case 的寫法漂亮多了,於是我又另外刻了createReducer 這樣的 helper,至於 createTypes / createActions 我嫌囉嗦,打算 dispatch 的時候直接 hard code { type: ‘ACTION_TYPE’ } ,反正我們的dispatch 的 action 不多,實在無需 Action Creator 這樣脫褲子放屁…

Create Reducer 的部分

另外 immutable 之類的工具也不用了,反正記得 reducer 回傳的 state 得是一個新的 object 不然是不會動的,有需要搭配使用的同學就自由發揮吧

這邊是合併 reducers 的部分

Data persist 的部分參考了原先使用的 redux-persist 介面寫了自家用解法,這些小工具我也不好意思獻醜拉,如果有需要上述 helpers (combineReducers/createReducers/persistReducer)的同學可在底下留言,看需要的人多的話我再來發 gist

我們來看看這在元件中如何應用,首先跟使用 redux-react 一樣,在最外部我們要用一個 Context.Provider 包住

然後在某個子元件如要使用的話,可以透過 Context.Consumer render props,不過既然我們都用了Hooks, 何不直接使用 useContext 更快,個人認為這樣的寫法比 Consumer render props 或 HOC 的解法都來的乾淨

整個 import 步驟有點多,其實可以把分散的 Code 整理整理放到同一個檔案方便外部引入

這邊我用了一個小伎倆,named export 是給 Provider,default export 則是給 Consumer,因為多半情境你會比較常 import 後者,前者只有在 app.js會需要設定一次

在 app.js 會變成這樣

元件裡引用則變這樣

如此一來是不是就更簡潔了呢? 好了我們這邊一口氣解決了兩個需求

2 down 2 to go :D

useBreakpoints

在講重頭戲 ajax request 之前先來個相較簡單的當作中場休息,這趴來說說如何自幹 Hooks 取代 redux-responsive(迷之音:我褲子都脫惹結果你給我跳話題…)

redux-responsive 使用上你可以設定 breakpoints,然後它在 redux state tree 內提供了一整組 boolean flags 讓你可以做查照,在元件裡就可以做類似下面的事

<div className={browser.lessThan.medium ? 'col-1' : 'col-3'}>
...
</div>

在 react-use 的專案中我看到了 useMedia 這樣的 Hooks,基於 Hooks 不過是函數,你可以利用原有的 Hooks 做 composition,於是借用了 useMedia 寫了如下的代碼

useMedia 這樣的 hooks 如果直接使用在各個元件中,每次使用都會訂閱 matchMedia onChange event,這樣一來會做太多不必要的訂閱,其實只要在 App level 訂閱一次,其結果再用 Context 傳遞到子元件即可,如此一來就能省去重複的event listener,於是在之上我又多加了 Context,設置方法就跟上面的 useAppReducer 差不多

在子元件中使用的方法如下

import useBreakpoints from 'Hooks/useBreakpoints'function Foo () {
const breakpoints = useBreakpoints()
// breakpoints.lessThan.md is equivalent to browser.lessThan.medium

我覺得這是體現 Hooks 概念強大的一個例子,Composition + Reusability ,有了 Hooks 很多邏輯都能抽出來重複運用,如果說 Component 是視圖的樂高積木,那 Hooks 就是商業邏輯的樂高積木了。

什麼?可以在元件中直接呼叫 ajax request?這合法嗎?

其實打從用 react 以來我一直沒有細究說為什麼不能從元件中直接呼叫 ajax request,大家開始學的時候透過 redux 去串 thunk/saga 來處理 ajax request 彷彿就是理所當然的事。

我能想到的大致上是怕說這些非同步的 response callback 如果執行時元件已經不在了會造成潛在的 memory leak,我猜是這樣,歡迎留言分享正解,感謝大大無私分享,施主好人一生平安

不過現在我們有了 useEffect ,你可以大大方方在元件內處理 side effect 了,以下用一個 Sign In form 來做為探討案例

ajax request 的觸發是透過一個 pending 的 flag 來操作,我們可以用一個 if statement 確保在 componentDidMount 的階段不會發出 request ,只有當 pending 由 false -> true 的時候會觸發

接下來元件中不管是 button onClick 或是 form onSubmit,要做的事就很簡單,只是單純 setPending(true) 這樣,記得 request succeed/failed 要把 flag set 回 false

網路上有些案例是利用 setUrl 來做觸發,其實概念上大同小異,大家可自由發揮,不過建議是集中在 Route level 來處理這些 Ajax side effect,你如果要把層級拉到 App level,包一層 Network Component,然後搭配使用前面說的 useReducer/useContext 也是可行,那樣就更像以前的 redux 寫法

npm uninstall redux. Yes, you can.

記得前不久在 ReactJS.tw 社群,有人來叫板戰 vue vs react 優劣,結果裡面一半內容是在批評 redux 有多累贅難懂,我承認 redux 發展了這麼多年,生態系膨脹到有點讓人難以上手,但它的歷史定位是無庸置疑的

只是該走下神壇的總會走下神壇,react 生態系不是注定要跟 redux 鐵板一塊,之所以熱愛 react 最大原因就是看著它不斷進化,週邊工具可能殞落,但主角依舊屹立不搖

整個改寫過程下來並沒有太多的不適應,把 Class Component 改成 Function Component 再串上 Hooks 其實蠻簡單的,而且你也不用一次到位,有用到 Hooks 再改就好

這篇文章希望能幫助到還在觀望的人,我自己先當白老鼠把 redux 給拔了,效果拔擢,同事表示喜歡,我自己也覺得不賴…

所以說,你準備好 Hooks 了嗎?

--

--

pxn

闖蕩矽谷十餘年的打工仔,目前是 Everbridge 的 UX Architect,如果有現成輪子就懶得自己動手的務實派,但沒有輪子的時候,就隨手造個火箭出來吧 🚀