redux vs useContext, useReducer,該怎麼選擇?

前言

React 是 UI Library,不像 Vue.Js、Angular 是完整的 framwork
在複雜頁面上,僅靠原本的 prop drilling,難以維護
主流是倚靠 redux 或 mobx 來做中央資料管理
避免深層傳遞

React 釋出 useContext, useReducer 後,網路上可以找到許多關於他們可以取代 redux 的討論
通常都是說在小專案的話,是完全不需要 redux 的

但小的規模是多小呢?
各種討論看來看去,也找不到一個 best pratice
對於初學者來說,真的是一頭霧水…

redux 好複雜,可以取代 redux 嗎?

引用為什麼需要使用Redux在專案上部份內容:

讓我們來看看React官方的一段描述:
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
Context是一種Dependency Injection設計,請注意,它並沒有管理狀態,它是一種傳輸的機制
Context和Redux都可以存取資料與追蹤變數的改變,但是Context無法做到更新資料這件事情
他能做到的就是透過props傳遞到Provider
這也是為什麼Context的出現無法取代Redux的原因之一

可以肯定的是,現階段 useContext, useReducer 是完全無法取代 redux 的!
因為 useContext 有個致命的問題 ── 當 store 異動時,所有使用useContext的組件,全部都會渲染!

redux 則是只有使用到異動值的組件才會渲染!

所以初學時有些細節沒搞清楚的話
會發現你隨便寫個簡單的操作,就很輕易的造成無限渲染(Infinite Render)

再想想本文的第一句:React 是 UI Library,不是 framework
雖然現在有著大量的生態系圍繞著他,讓他組裝成一完整的 framework,解決各種需求

至少到目前為止,他並沒有想要解決這層問題,因為社群已有很好的解法:即 redux 或 mobx

我還是不想用 redux,React 原生的解法該怎麼做?

useContext, useReducer 是其中一種解法
但並不是官方希望你用的解法
因為他的目的是解決靜態資料深層 props drilling 的問題。並不是狀態管理

若你仍堅持努力試著解決無限 Render的問題,你會發現越寫越複雜
一堆東西要包著useCallbackuseMemo,甚至有些就算是包了,仍然沒有解決問題!


React 與另 2 家前端框架最大的不同之處就是:單向資料流 ── 資料只有往下,不會往上
所以解法就是把所有參數、函式,往上提升 (lifting state up)

就算是規模不大的小專案,會發現很容易就把一堆狀態、函式提升到最上層
所有的邏輯包含 function 都在根層了!

對於有使用過 Vue.Js、Angular 經驗的開發者來說
會有點不太習慣 ── 因為組件自己的邏輯並沒有封裝在自己身上,跑到外面去了!

但這就是原生 React 的運作方式 ── 封裝的只有自己的 UI 與純粹自用的邏輯。當邏輯有與其他組件有相依性時,就是往上找

當透過 useContext, useReducer 的無限 Render 已經困擾到你開發不下去、而仍不想使用 redux 的話
你只剩一招:往上提升 (lifting state up)
至少對於規模不大的小專案來說,還不至於到難以閱讀


若先前有開發過 Vue.Js 的經驗
應該都有在官方文件看過:vuex 就像眼鏡,當你需要的時候才用上他

而 vuex 就是參考 redux(ps: 現在 Vue 3 已推薦使用Pinia)
那 redux 是不是也是呢?

若 vue 用上 vuex 的話,可能是已近視 3、400 度。不戴雖然還是看的見(僅靠 vue 本身仍還算可做),但就是有那麼一點不舒服
而 React 用上 redux,大約就是當你 200 度,就該用上他了…不然你就會看到所有東西都提升到根層去

快速認識 useContext, useReducer

  1. useContext的出現,起初是為了解決靜態資料深層 props drilling 的問題,並不是狀態管理。只是可以搭配useReducer來達到類似 redux 的功能。並不是原本的目的
  2. useReducer是解決太多變數的問題,即一個組件裡有太多相似性質的useState,可以放在useReducer整個包起來,並將 pure function 邏輯封裝起來,達到 React 裡只需要dispatch任務
  3. useContext.Provder的store,是搭配useReducer(即把useReducer產出來的initStore, dispatch一起傳進 provider 的 store),做到類似 redux 的事情
  4. useContext.Provder,下層會蓋掉上層,不會合併。使用時要注意層級關係
  5. useContext.Provder包覆底下,未使用useContext的,不會重覆渲染
  6. 官方推薦避免無限渲染的解法:
    1. 分離多個 provider (亦是 React 官方推薦解法)。但有的時候也許並沒那麼好拆解
    2. 改用 prop 傳遞(若深度不深)
    3. 使用 rdeux 或 mobx
  7. 他們本來是 2 個不同用途的 hook,只是可以組合使用,變成一個弱化版的 redux
  8. 熟悉的useState,底層其實是 Call useReducer

快速認識 redux

  1. redux 不是為了 React 發明的,只是恰巧發現他很適合 React
  2. 現在請使用@reduxjs/toolkit,已簡化許多步驟,且直接內置redux-thunk,可以將異步函式放在這裡
  3. 不像useContext可以有好多個 Provider,而下層會覆蓋掉上層。通常 redux 只會有一個 store,且放在最根層
  4. reducer 只能是 pure function
  5. redux 的底層,其實也有使用到 Context
  6. redux 可以讓 React 組件變得很乾淨:useSelector拿資料、useDispatch指派任務。可以把大部份邏輯都集中封裝在 redux 裡
  7. 一個簡易 redux/toolkit 資料流程:
    1. slice 定義好 reducer 功能,籍由 toolkit 自動產生 action 並 export 出去供 React 組件使用
    2. React 組件裡,Call dispatch(action)
    3. 【reducer 更新 store 相關值】或【thunk 執行異步任務,再到 extraReducer 更新 store 相關值】
    4. 更新那些有透過useSelector使用到本次異動值的 React 組件

心得

在學習 React.Js 之前,我本來就相當熟悉 Angular,也大略知道 Vue.Js 的基本開發
想說在 Side Project 來試試唯一沒碰過、卻享有最大生態系的 React
我自己的開發習慣是能不裝套件就不裝,盡可能用原生語法或框架原生函式來實現需求

最初是完全不打算使用 redux 的
結果在 useContext, useReducer 無限渲染大卡關

父子組件皆有使用到Context的值,但子組件 dispatch 後,造成父組件再次渲染
父組件一渲染,子組件再度刷新….

而 store 裡的內容有高度相依性,我又不想要把他們拆成 2 個
最後確認不透過 redux,最終解法就是往上提升 (lifting state up)
當然也是最簡單的解法,甚至不用費心研究 useContext, useReducer

但這樣就只是很單純的傳值運算,感覺不出有深入使用 React

Side Project 規模確實小
但 form 裡的欄位很多,且欄位內容互相皆有高度相依性(form裡只要改動一個欄位,皆要 Call 異步函式計算輸出結果,然後呈現在畫面上)
所有 function 都提上去,那根組件就會很大一包

雖然可以包成 hook 再把整個邏輯抽離出去,這樣根組件就會精簡許多

而這一段就跟 Angular 有很大的不同
也是我思考盲點 ── 我一直想要把 function 封在自己的組件裡,只把處理完的結果丟出去(即放在useContext裡)。而因資料的高度相依性不想拆成多個Context,何況拆了也沒把握是不是可以順利解決XD

最後決定使用 redux
也更深入的感受到他們之間並不是互相取代,而是各自解決的不同的問題點

至少現階段 React 並沒有想要自己解決狀態管理的問題
他還是在他一慣的道路上:UI Library

React 的單純性,在純 UI 面的開發,爽度真的比 Angular 高上許多
All-in-JS 的特性,在一些邏輯的處理上,就是很單純的 if 判斷式,少了許多特殊指令

參考資料

非常推薦閱讀下列 2 篇文章,讓我一口氣釐清所有卡關的點!

The Problem with React’s Context API
為什麼需要使用Redux在專案上