去年一年作了很多 React 和 React Native 項目的開發,並且這些項目都使用了 Redux 來管理組件狀態 。碰巧,這些項目裏有不少具備表明性的開發模式,因此趁着我還在 Wolox,在分析、總結了這些模式以後,開發出了 redux-recompose,算是對這些模式的抽象和提高。javascript
在 Wolox 培訓的那段時間,爲了學 redux 看了 Dan Abramov’s 在 Egghead 上發佈的 Redux 教程,發現他大量使用了 switch
語句:我聞到了點 壞代碼的味道。前端
在我接手的第一個 React Native 項目中,開始的時候我仍是按照教程上講的,使用 switch
編寫 reducer。但不久後就發現,這種寫法實在難以維護:java
import { actions } from './actions';
const initialState = {
matches: [],
matchesLoading: false,
matchesError: null,
pitches: [],
pitchesLoading: false,
pitchesError: null
};
/* eslint-disable complexity */
function reducer(state = initialState, action) {
switch (action.type) {
case actions.GET_MATCHES: {
return { ...state, matchesLoading: true };
}
case actions.GET_MATCHES_SUCCESS: {
return {
...state,
matchesLoading: false,
matchesError: null,
matches: action.payload
};
}
case actions.GET_MATCHES_FAILURE: {
return {
...state,
matchesLoading: false,
matchesError: action.payload
};
}
case actions.GET_PITCHES: {
return { ...state, pitchesLoading: true };
}
case actions.GET_PITCHES_SUCCESS: {
return {
...state,
pitches: action.payload,
pitchesLoading: false,
pitchesError: null
};
}
case actions.GET_PITCHES_FAILURE: {
return {
...state,
pitchesLoading: false,
pitchesError: null
};
}
}
}
/* eslint-enable complexity */
export default reducer;
複製代碼
到後面 reducer 裏的條件實在是太多了,索性就把 eslint 的複雜度檢測關掉了。react
另外一個問題集中在異步調用上,action 的定義中大量充斥着 SUCCESS 和 FAILURE 這樣的代碼,雖然這可能也不是什麼問題,可是仍是引入了太多重複代碼。android
import SoccerService from '../services/SoccerService';
export const actions = createTypes([
'GET_MATCHES',
'GET_MATCHES_SUCCESS',
'GET_MATCHES_FAILURE',
'GET_PITCHES',
'GET_PITCHES_SUCCESS',
'GET_PITCHES_FAILURE'
], '@SOCCER');
const privateActionCreators = {
getMatchesSuccess: matches => ({
type: actions.GET_MATCHES_SUCCESS,
payload: matches
}),
getMatchesError: error => ({
type: actions.GET_MATCHES_ERROR,
payload: error
}),
getPitchesSuccess: pitches => ({
type: actions.GET_PITCHES_SUCCESS,
payload: pitches
}),
getPitchesFailure: error => ({
type: actions.GET_PITCHES_FAILURE,
payload: error
})
};
const actionCreators = {
getMatches: () => async dispatch => {
// 將 loading 狀態置爲 true
dispatch({ type: actions.GET_MATCHES });
// -> api.get('/matches');
const response = await SoccerService.getMatches();
if (response.ok) {
// 存儲 matches 數組數據,將 loading 狀態置爲 false
dispatch(privateActionCreators.getMatchesSuccess(response.data));
} else {
// 存儲錯誤信息,將 loading 狀態置爲 false
dispatch(privateActionCreators.getMatchesFailure(response.problem));
}
},
getPitches: clubId => async dispatch => {
dispatch({ type: actions.GET_PITCHES });
const response = await SoccerService.getPitches({ club_id: clubId });
if (response.ok) {
dispatch(privateActionCreators.getPitchesSuccess(response.data));
} else {
dispatch(privateActionCreators.getPitchesFailure(response.problem));
}
}
};
export default actionCreators;
複製代碼
某天,個人同事建議:ios
’要不試試把 switch
語句改爲訪問對象屬性的形式?這樣以前 switch
的條件就都能抽離成單個的函數了,也方便測試。‘git
再者,Dan Abramov 也說過: Reducer 就是一個很普通的函數,你能夠抽出一些代碼獨立成函數,也能夠在裏面調用其餘的函數,具體實現能夠自由發揮。github
有了這句話咱們也就放心開幹了,因而開始探索有沒有更加優雅的方式編寫 reducer 的代碼。最終,咱們得出了這麼一種寫法:npm
const reducerDescription = {
[actions.GET_MATCHES]: (state, action) => ({ ...state, matchesLoading: true }),
[actions.GET_MATCHES_SUCCESS]: (state, action) => ({
...state,
matchesLoading: false,
matchesError: null,
matches: action.payload
}),
[actions.GET_MATCHES_FAILURE]: (state, action) => ({
...state,
matchesLoading: false,
matchesError: action.payload
}),
[actions.GET_PITCHES]: (state, action) => ({ ...state, pitchesLoading: true }),
[actions.GET_PITCHES_SUCCESS]: (state, action) => ({
...state,
pitchesLoading: false,
pitchesError: null,
pitches: action.payload
}),
[actions.GET_PITCHES_FAILURE]: (state, action) => ({
...state,
pitchesLoading: false,
pitchesError: action.payload
})
};
複製代碼
function createReducer(initialState, reducerObject) {
return (state = initialState, action) => {
(reducerObject[action.type] && reducerObject[action.type](state, action)) || state;
};
}
export default createReducer(initialState, reducerDescription);
複製代碼
SUCCESS 和 FAILURE 的 action 和以前看來沒啥區別,只是 action 的用法變了 —— 這裏將 action 和操做它對應的 state 裏的那部分數據的函數進行了一一對應。例如,咱們分發了一個 action.aList 來修改一個列表的內容,那麼‘aList’就是找到對應的 reducer 函數的關鍵詞。redux
有了上面的嘗試,咱們不妨更進一步思考:何不站在 action 的角度來定義 state 的哪些部分會被這個 action 影響?
咱們能夠把 action 想象成一個「差使」,action 不關心 state 的變化 —— 那是 reducer 的事。
那麼,爲何就不能反其道而行之呢,若是 action 就是要去管 state 的變化呢?有了這種想法,咱們就能引伸出 靶向化 action 的概念了。何謂靶向化 action?就像這樣:
const privateActionCreators = {
getMatchesSuccess: matchList => ({
type: actions.GET_MATCHES_SUCCESS,
payload: matchList,
target: 'matches'
}),
getMatchesError: error => ({
type: actions.GET_MATCHES_ERROR,
payload: error,
target: 'matches'
}),
getPitchesSuccess: pitchList => ({
type: actions.GET_PITCHES_SUCCESS,
payload: pitchList,
target: 'pitches'
}),
getPitchesFailure: error => ({
type: actions.GET_PITCHES_FAILURE,
payload: error,
target: 'pitches'
})
};
複製代碼
若是你之前用過 redux saga 的話,應該對 effects 有點印象,但這裏要講的還不是這個 effects 的意思。
這裏講的是將 reducer 和 reducer 對 state 的操做進行解耦合,而這些抽離出來的操做(即函數)就稱爲 effects —— 這些函數具備冪等性質,並且對 state 的變化一無所知:
export function onLoading(selector = (action, state) => true) {
return (state, action) => ({ ...state, [`${action.target}Loading`]: selector(action, state) });
}
export function onSuccess(selector = (action, state) => action.payload) {
return (state, action) => ({
...state,
[`${action.target}Loading`]: false,
[action.target]: selector(action, state),
[`${action.target}Error`]: null
});
}
export function onFailure(selector = (action, state) => action.payload) {
return (state, action) => ({
...state,
[`${action.target}Loading`]: false,
[`${action.target}Error`]: selector(action, state)
});
}
複製代碼
注意上面的代碼是如何使用這些 effects 的。你會發現裏面有不少 selector 函數,它主要用來從封裝對象中取出你須要的數據域:
// 假設 action.payload 的結構是這個樣子: { matches: [] };
const reducerDescription = {
// 這裏只引用了 matches 數組,不用處理整個 payload 對象
[actions.GET_MATCHES_SUCCESS]: onSuccess(action => action.payload.matches)
};
複製代碼
有了以上思想,最終處理函數的代碼變成這樣:
const reducerDescription = {
[actions.MATCHES]: onLoading(),
[actions.MATCHES_SUCCESS]: onSuccess(),
[actions.MATCHES_FAILURE]: onFailure(),
[actions.PITCHES]: onLoading(),
[actions.PITCHES_SUCCESS]: onSuccess(),
[actions.PITCHES_FAILURE]: onFailure()
};
export default createReducer(initialState, reducerDescription);
複製代碼
固然,我並非這種寫法的第一人:
到這一步你會發現代碼仍是有重複的。針對每一個基礎 action(有配對的 SUCCESS 和 FAILURE),咱們仍是得寫相應的 SUCCESS 和 FAILURE 的 effects。 那麼,可否再作進一步改進呢?
Completer 能夠用來抽取代碼中重複的邏輯。因此,用它來抽取 SUCCESS 和 FAILURE 的處理代碼的話,代碼會從:
const reducerDescription: {
[actions.GET_MATCHES]: onLoading(),
[actions.GET_MATCHES_SUCCESS]: onSuccess(),
[actions.GET_MATCHES_FAILURE]: onFailure(),
[actions.GET_PITCHES]: onLoading(),
[actions.GET_PITCHES_SUCCESS]: onSuccess(),
[actions.GET_PITCHES_FAILURE]: onFailure(),
[actions.INCREMENT_COUNTER]: onAdd()
};
export default createReducer(initialState, reducerDescription);
複製代碼
變成如下更簡潔的寫法:
const reducerDescription: {
primaryActions: [actions.GET_MATCHES, actions.GET_PITCHES],
override: {
[actions.INCREMENT_COUNTER]: onAdd()
}
}
export default createReducer(initialState, completeReducer(reducerDescription))
複製代碼
completeReducer
接受一個 reducer description 對象,它能夠幫基礎 action 擴展出相應的 SUCCESS 和 FAILURE 處理函數。同時,它也提供了重載機制,用於配製非基礎 action 。
根據 SUCCESS 和 FAILURE 這兩種狀況定義狀態字段也比較麻煩,對此,可使用 completeState
自動爲咱們添加 loading 和 error 這兩個字段:
const stateDescription = {
matches: [],
pitches: [],
counter: 0
};
const initialState = completeState(stateDescription, ['counter']);
複製代碼
還能夠自動爲 action 添加配對的 SUCCESS
和 FAILURE
:
export const actions = createTypes(
completeTypes(['GET_MATCHES', 'GET_PITCHES'], ['INCREMENT_COUNTER']),
'@@SOCCER'
);
複製代碼
這些 completer 都有第二個參數位 —— 用於配製例外的狀況。
鑑於 SUCCESS-FAILURE 這種模式比較常見,目前的實現只會自動加 SUCCESS 和 FAILURE。不過,後期咱們會支持用戶自定義規則的,敬請期待!
那麼,異步 action 的支持如何呢?
固然也是支持的,多數狀況下,咱們寫的異步 action 無非是從後端獲取數據,而後整合到 store 的狀態樹中。
寫法以下:
import SoccerService from '../services/SoccerService';
export const actions = createTypes(completeTypes['GET_MATCHES','GET_PITCHES'], '@SOCCER');
const actionCreators = {
getMatches: () =>
createThunkAction(actions.GET_MATCHES, 'matches', SoccerService.getMatches),
getPitches: clubId =>
createThunkAction(actions.GET_PITCHES, 'pitches', SoccerService.getPitches, () => clubId)
};
export default actionCreators;
複製代碼
思路和剛開始是同樣的:加載數據時先將 loading 標誌置爲 true
,而後根據後端的響應結果,選擇分發 SUCCESS 仍是 FAILURE 的 action。使用這種方法,咱們抽取出了大量的重複邏輯,也不用再建立 privateActionsCreators
對象了。
可是,若是咱們想要在調用和分發過程當中間執行一些自定義代碼呢?
咱們可使用 注入器(injections) 來實現,在下面的例子中咱們就用這個函數爲 baseThunkAction 添加了一些自定義行爲。
這兩個例子要傳達的思想是同樣的:
const actionCreators = {
fetchSomething: () => async dispatch => {
dispatch({ type: actions.FETCH });
const response = Service.fetch();
if (response.ok) {
dispatch({ type: actions.FETCH_SUCCESS, payload: response.data });
dispatch(navigationActions.push('/successRoute');
} else {
dispatch({ type: actions.FETCH_ERROR, payload: response.error });
if (response.status === 404) {
dispatch(navigationActions.push('/failureRoute');
}
}
}
}
複製代碼
const actionCreators = {
fetchSomething: () => composeInjections(
baseThunkAction(actions.FETCH, 'fetchTarget', Service.fetch),
withPostSuccess(dispatch => dispatch(navigationActions.push('/successRoute'))),
withStatusHandling({ 404: dispatch => dispatch(navigationActions.push('/failureRoute')) })
)
}
複製代碼
以上是對這個庫的一些簡介,詳情請參考 github.com/Wolox/redux…。 安裝姿式:
npm install --save redux-recompose
複製代碼
感謝 Andrew Clark,他建立的 recompose 給了我不少靈感。同時也感謝 redux 的創始人 Dan Abramov,他的話給了我不少啓發。
固然,也不能忘了同在 Wolox 裏的戰友們,是你們一塊兒協力才完成了這個項目。
歡迎各位積極提出意見,若是在使用中發現任何 bug,必定要記得在 GitHub 上給咱們反饋,或者提交你的修復補丁,總之,我但願你們都能積極參與到這個項目中來!
在之後的文章中,咱們將會討論更多有關 effects、注入器(injectors)和 completers 的話題,同時還會教你如何將其集成到 apisauce 或 seamless-immutable 中使用。
但願你能繼續關注!
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。