簡化 Redux 狀態管理

事情是怎麼變複雜的

Redux 本來並不複雜,其基本理念能夠歸納爲:經過 action 提交變動,通過 reducer 計算出新的 state。action 是一個約定含 type 字段的對象,reducer 是一個約定以 state 和 action 爲參數、以新的 state 爲返回值的純函數。javascript

state -> dispatch(action) -> reducer -> new State => new View
複製代碼

考慮最原始的情形,在業務代碼中直接 dispatch action 對象,那麼只須要定義三個文件就能夠了:state.js、reducer.js、store.js,分別用於定義 state、reducer 和建立 store。html


一、不要直接提交 action 對象

業務代碼直接 dispatch action 對象並很差,一是重複,二是無類型校驗。官方推薦用函數來建立 action,而且 action 的 type 最好用常量而不是字符串,同時還需保證 type 的全局惟一性。因而再加兩個文件:actions.js、type.js,分別用於定義 action 函數和 type 常量。vue

這就造成了廣泛遭受詬病的模板代碼(常量還得全大寫,害),因而出現了一些方案,來簡化 action 與 type 的定義,好比 reduxsause、react-arc。java


二、reducer 須要拆開

官方僅僅約定了 reducer 的 interface,但沒有規定具體實現。原始案例中用 switch,每一個 case 對應一個 action,當 action 增多時,一個超長的 reducer 顯然是不利於維護的。官方提供了一種拆分思路,並提供了相應的輔助函數(combineReducers)。react

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}
複製代碼

不過這種模式有不少問題,好比:每一個字段都須要定義一個子 reducer;子 reducer 的 state 參數都不同,而且不能忘了設置初始值;同一個 action 可能分佈在不一樣的子 reducer 中,每新增一個 action,若是對應多個字段的變動,那麼須要在多個 reducer 中新增 case 分支。git


三、深層數據結構如何更新

redux 約定 state 必須是全局惟一而且是 immutable 的(約定,而非約束),reducer 每次都須要整個的返回一個新的 state。若是數據結構比較深,更新起來很麻煩。這個問題相對好解決,一般用 immutable-helper、seamless-immutable 之類的輔助庫便可。github


四、異步更新邏輯怎麼辦

好像全部介紹 Redux 的文章都會不約而同的宣稱一個極具誤導性的論斷:Redux 不支持異步狀態更新。vuex

那是否是說,若是單純用 Redux,應用裏的異步邏輯就沒法寫了?顯然沒有這回事。typescript

// 照常寫異步邏輯,而後提交更新,有啥問題咩?
fetch().then(data => {
    dispatch(updateAction(data))
})
複製代碼

不過有人會這麼想:我就想提交一個 action,由這個 action 去完成異步邏輯,人家 vuex 和 mobX 都有相關的支持,redux 咋就不行?redux

因而官方經過 redux-thunk 提供了一個語法糖,讓你能夠 dispatch 一個封裝了異步邏輯的 thunk 函數,使得代碼在語義上可以實現「提交了一個異步 action」這麼個事兒。

// 寥寥數行的 redux-thunk 中間件
function createThunkMiddleware() {
  return ({ dispatch, getState }) => (next) => (action) => {
    if (typeof action === 'function') { // 啊哈
      return action(dispatch, getState)
    }
    return next(action)
  };
}
複製代碼

看清本質你就會明白,哪裏有「redux 不支持異步狀態更新」這回事兒呢?只是代碼邏輯聚合在哪裏的區別而已,最終要觸發 reducer 更新 state,仍是得在 dispatch 一個對象的時候。因此根本不存在「非得引一個庫 redux 才能支持異步」這回事兒,不管是 redux-thunk、redux-promise 仍是 redux-saga,與其說在彌補 redux 的缺陷,倒不如說它們在解決本身額外創造出來的問題。

偏激的講,【async action】 是一個與 redux 無關的概念陷阱。客觀的說,【async action】是一種代碼設計抽象。原本 action 就是一個純對象而已,如今 action 還能夠是一個函數,被稱爲異步 action。

const a = { type, payload } // 這是一個 reducer action
const b = payload => ({ type, payload }) // 這也是一個 reducer action
const c = dispatch => fetch().then(res => dispatch(xxx) // 這是一個 async action
複製代碼

任何狀態管理庫,提供的所謂異步狀態更新功能,都只是一種 api 層面上的包裝。這固然有好處,好比提供了額外的抽象約束,狀態更新邏輯更加內聚,同時讓業務邏輯更純粹,組件只管發 action 和呈現數據。但不能認爲「redux 不支持異步 action,因此必須加 thunk,必須加 saga」,這是站在抽象的高峯,被概念的雲霧迷了眼。


Why do we need middleware for async flow in Redux?


小結

本來只是三個文件的事,最終擴展成了 redux + react-redux + action type 定義方案 + immutable 數據更新方案 + 異步過程封裝方案。這下好了,要寫一個最簡單的「請求接口而後更新數據」的邏輯,每每須要改動 五、6 個文件。整個邏輯鏈路之長,不只寫起來費勁,看/找起來也費勁。

因而社區給出了整合、封裝甚至重構過的相對完整的類 redux 方案,好比 rematch(Redesigning Redux)、dva。更激進的,有人不肯意受「只能經過 dispatch action 而不是直接調用 reducer 更新 state」的約束,搞出了【action reducer 化】的方案,好比 redux-zero、reduxless。

// redux
dispatch(action) => reducer => newState

// redux-zero
action(state, payload) => newState
複製代碼

圍繞 redux,不只有大量「修補型」方案,還有很多「整合型」、「重構型」和「替代型」方案,正面的說,這是生態繁榮的體現,負面的說,這是對 redux 做爲事實上的【react 狀態管理業界標準】的某種諷刺。這一切究竟是證實 redux 自己不是一個好的設計,仍是說使用者沒用對,把事情搞複雜了?


一種自動生成 action 的簡化方案

每個 action,必定對應一個 reducer 處理邏輯,若是 reducer 函數按 action 粒度拆分,每一個 action,對應一個 reducer 函數,而每一個 action 擁有惟一的 type,那麼能夠得出:action、type 和 reducer 是一一對應的。

若是能保證 reducer 不重名,而後 action 和 type 直接複用 reducer 的名稱,那麼 action 就能根據 reducer 自動生成。

// reducer.js
// reducer 的拆分方式有不少
// 這裏每一個 reducer 對應一個 action
// 第一個參數是全局的 state,第二個參數對應 action 中的 payload
export const reducerA = (state, payload) => state
export const reducerB = (state, payload) => state

// action.js
// 指望根據 reducer 自動生成的 actions 對象
export const actions = {
    reducerA: payload => ({ type: 'reducerA', payload }),
    reducerB: payload => ({ type: 'reducerB', payload })
}
複製代碼

首先,按上述方式拆分的 reducer,須要按以下方式聚合:

// store.js
import initialState from './state.js'
import * as reducers from './reducer.js'

// 聚合 reducer
function reducer(state = initialState, { type, payload }) {
  const fn = reducers[type];
  return fn ? fn(state, payload) : state;
}
複製代碼

上述寫法要求全部的 reducer 都聚合在 reducer.js 裏,注意,不必定都定義在 reducer.js 裏,能夠分散定義在不一樣文件中,只是在 reducer.js 裏統一導出,這樣就保證了 reducer 不會重名。


有了 reducer 的 map 對象,很容易自動生成 actions 對象:

// action.js
import * as reducers from './reducer.js'

export const actions = Object.keys(reducers).reduce(
  (prev, type) => {
    prev[type] = payload => ({ type, payload })
    return prev
  },
  {}
)
複製代碼

使用的時候,引入 actions 對象便可:

import { actions } from 'store/action.js'

dispatch(actions.reducerA(payload))
複製代碼

這樣,action 和 type 就都不須要定義了。每次新增邏輯,狀態部分就只須要寫 state 和 reducer 便可。

不過上述方式仍然不夠完美,由於沒有類型。按常規的寫法,action 是有類型定義的,既能夠校驗參數,又有自動補全提示,可丟不得。

// reducer.ts
export interface A {}
export const reducerA = (state: StateType, payload: A) => state

// action.ts
// 常規寫法
import { A } from './reducer.ts'
export const actionA = (payload: A) => ({ type: TYPE, payload })
複製代碼

考慮到 action 函數的參數類型和對應 reducer 第二個參數的類型是一致的,那麼可否既複用 reducer 的名稱,又複用參數類型,在自動生成 actions 對象的同時,連類型也一塊兒自動生成呢?

// 指望生成的 interface
// 關鍵是如何拿到 reducer 函數定義好的 payload 參數的類型,返回值類型其實不須要關心
interface Actions {
    reducerA (payload: A): AnyAction
    reducerB (payload: B): AnyAction
}
複製代碼

首先不考慮 payload 參數類型,先看如何自動生成 actions 對象的 interface:

可見,經過 keyof 關鍵字,Actions 類型已經拿到了全部的鍵值。

接下來要設置 payload 參數類型,關鍵是如何拿到一個已知的函數類型定義中的第二個參數的類型。TS 裏提供了 infer 關鍵字用於提取類型。

T 表示 args 的類型,是一個數組,T[1] 即第二個參數的類型。

搞定了以上兩個關鍵步驟,剩下的事情就比較簡單了:

這樣咱們就有了類型,每次新增 reducer,都會自動生成最新的 actions 及其類型。

省略了 action 定義,也就省掉了模板代碼中的一大半。其它減省代碼的地方還有:mapDispatchToProps 使用官方推薦的簡寫形式、用 class 定義 state 以直接提取 StateType 類型、封裝 store 定義等,細枝末節很少贅述。

完整案例可參見:codesandbox.io/s/clever-ra…

相關文章
相關標籤/搜索