Redux進階系列文章:
1. React+Redux項目結(jié)構最佳實踐
2. 如何合理地設計State
在前面兩篇文章中剩彬,我們介紹了Redux項目結(jié)構的組織方式和如何設計State。本篇矿卑,我們將以前面兩篇文章為基礎喉恋,繼續(xù)介紹如何設計action、reducer母廷、selector轻黑。
依然以博客項目為例,我們在第2篇中最后設計的state結(jié)構如下:
{
"app":{
"isFetching": false,
"error": "",
},
"posts":{
"byId": {
"1": {
...
},
...
},
"allIds": [1, ...],
}
"comments": {
...
},
"authors": {
...
}
}
根據(jù)這個結(jié)構琴昆,我們很容易想到可以拆分成4個reducer分別處理app氓鄙、posts、comments业舍、authors這4個子state玖详。子state相關的action和這個state對應的reducer放到一個文件中,作為一個state處理模塊勤讽。注意:本文定義的action、reducer拗踢、selector并不涵蓋真實博客應用中涉及的所有邏輯脚牍,僅列舉部分邏輯,用以介紹如何設計action巢墅、reducer诸狭、selector。
state中的 app 管理應用狀態(tài)君纫,應用狀態(tài)與領域狀態(tài)不同驯遇,領域狀態(tài)是應用用來顯示、操作的數(shù)據(jù)蓄髓,一般需要從服務器端獲取叉庐,例如posts、comments会喝、authors都屬于領域狀態(tài)陡叠;而應用狀態(tài)是與應用行為或應用UI直接相關的狀態(tài),例如當前應用中是否正在進行網(wǎng)絡請求肢执,應用執(zhí)行時的錯誤信息等枉阵。app 包含的應用狀態(tài)有:isFetching(當前應用中是否正在進行網(wǎng)絡請求)和error(應用執(zhí)行時的錯誤信息)。對應的action可以定義為:
// 所在文件:app.js
//action types
export const types = {
const START_FETCH : 'app/START_FETCH',
const FINISH_FETCH : 'app/FINISH_FETCH',
const SET_ERROR : 'app/SET_ERROR'
}
//action creators
export const actions = {
startFetch: () => {
return {type: types.START_FETCH};
},
finishFetch: ()=> {
return {type: types.FINISH_FETCH};
},
setError: (error)=> {
return {type: types.SET_ERROR, payload: error};
}
}
types定義了app模塊使用的action types预茄,每一個action type的值以模塊名作為命名空間兴溜,以避免不同模塊的action type沖突問題。actions定義了該模塊使用到的action creators。我們沒有直接導出每一個action type和action creator拙徽,而是把所有的action type封裝到types常量刨沦,所有的action creators封裝到actions常量,再導出types和actions這兩個常量斋攀。這樣做的好處是方便在其他模塊中引用已卷。(在第1篇中已經(jīng)介紹過)
現(xiàn)在再來定義處理app的reducer:
// 所在文件:app.js
export const types = {
//...
}
export const actions = {
//...
}
const initialState = {
isFetching: false,
error: null,
}
// reducer
export default function reducer(state = initialState, action) {
switch (action.type) {
types.START_FETCH:
return {...state, isFetching: true};
types.FINISH_FETCH:
return {...state, isFetching: false};
types.SET_ERROR:
return {...state, error: action.payload}
default: return state;
}
}
現(xiàn)在,app.js就構成了一個基本的處理state的模塊淳蔼。
我們再來看下如何設計posts.js侧蘸。posts是這幾個子狀態(tài)中最復雜的狀態(tài),包含了posts領域數(shù)據(jù)的兩種組織方式:byId定義了博客ID和博客的映射關系鹉梨,allIds定義了博客在界面上的顯示順序讳癌。這個模塊需要使用異步action調(diào)用服務器端API,獲取博客數(shù)據(jù)存皂。當網(wǎng)絡請求開始和結(jié)束時晌坤,還需要使用app.js模塊中的actions,用來更改app中的isFetching狀態(tài)旦袋。代碼如下所示:
// 所在文件:posts.js
import {actions as appActions} from './app.js'
//action types
export const types = {
const SET_POSTS : 'posts/SET_POSTS',
}
//action creators
export const actions = {
// 異步action骤菠,需要redux-thunk支持
getPosts: () => {
return (dispatch) => {
dispatch(appActions.startFetch());
return fetch('http://xxx/posts')
.then(response => response.json())
.then(json => {
dispatch(actions.setPosts(json));
dispatch(appActions.finishFetch());
});
}
},
setPosts: (posts)=> {
return {type: types.SET_POSTS, payload: posts};
}
}
// reducer
export default function reducer(state = [], action) {
switch (action.type) {
types.SET_POSTS:
let byId = {};
let allIds = [];
/* 假設接口返回的博客數(shù)據(jù)格式為:
[{
"id": 1,
"title": "Blog Title",
"create_time": "2017-01-10T23:07:43.248Z",
"author": {
"id": 81,
"name": "Mr Shelby"
},
"comments": [{id: 'c1', authorId: 81, content: 'Say something'}]
"content": "Some really short blog content. "
}]
*/
action.payload.each((item)=>{
byId[item.id] = item;
allIds.push(item.id);
})
return {...state, byId, allIds};
default: return state;
}
}
我們在一個reducer函數(shù)中處理了byId和allIds兩個狀態(tài),當posts的業(yè)務邏輯較簡單疤孕,需要處理的action也較少時商乎,如上面的例子所示,這么做是沒有問題的祭阀。但當posts的業(yè)務邏輯比較復雜鹉戚,action類型較多,byId和allIds響應的action也不一致時专控,往往我們會拆分出兩個reducer抹凳,分別處理byId和allIds。如下所示:
// 所在文件:posts.js
import { combineReducers } from 'redux'
//省略無關代碼
// reducer
export default combineReducers({
byId,
allIds
})
const byId = (state = {}, action) {
switch (action.type) {
types.SET_POSTS:
let byId = {};
action.payload.each((item)=>{
byId[item.id] = item;
})
return {...state, byId};
SOME_SEPCIAL_ACTION_FOR_BYID:
//...
default: return state;
}
}
const allIds = (state = [], action) {
switch (action.type) {
types.SET_POSTS:
return {...state, allIds: action.payload.map(item => item.id)};
SOME_SEPCIAL_ACTION_FOR_ALLIDS:
//...
default: return state;
}
}
從上面的例子中伦腐,我們可以發(fā)現(xiàn)赢底,redux的combineReducers可以在任意層級的state上使用,而并非只能在第一級的state上使用(示例中的第一層級state是app柏蘑、posts颖系、comments、authors)辩越。
posts.js模塊還有一個問題嘁扼,就是byId中的每一個post對象,包含嵌套對象author黔攒。我們應該讓post對象只應用博客作者的id即可:
// reducer
export default function reducer(state = [], action) {
switch (action.type) {
types.SET_POSTS:
let byId = {};
let allIds = [];
action.payload.each((item)=>{
byId[item.id] = {...item, author: item.author.id};
allIds.push(item.id);
})
return {...state, byId, allIds};
default: return state;
}
}
這樣趁啸,posts只關聯(lián)博客作者的id强缘,博客作者的其他屬性由專門的領域狀態(tài)author來管理:
// 所在文件:authors.js
import { types as postTypes } from './post'
//action types
export const types = {
}
//action creators
export const actions = {
}
// reducer
export default function reducer(state = {}, action){
switch (action.type) {
postTypes.SET_POSTS:
let authors = {};
action.payload.each((item)=>{
authors[item.author.id] = item.author;
})
return authors;
default: return state;
}
這里需要注意的是,authors的reducer也處理了posts模塊中的SET_POSTS這個action type不傅。這是沒有任何問題的旅掂,一個action本身就是可以被多個state的reducer處理的,尤其是當多個state之間存在關聯(lián)關系時访娶,這種場景更為常見商虐。
comments.js模塊的實現(xiàn)思路類似,不再贅述⊙掳蹋現(xiàn)在我們的redux(放置redux模塊)目錄結(jié)構如下:
redux/
app.js
posts.js
authors.js
comments.js
在redux目錄層級下秘车,我們新建一個index.js文件,用于把各個模塊的reducer合并成最終的根reducer劫哼。
// 文件名:index.js
import { combineReducers } from 'redux';
import app from './app';
import posts from './posts';
import authors from './authors';
import commments from './comments';
const rootReducer = combineReducers({
app,
posts,
authors,
commments
});
export default rootReducer;
action和reducer的設計到此基本完成叮趴,下面我們來看selector。Redux中权烧,selector的“名聲”不如action眯亦、reducer響亮,但selector其實非常有用般码。selector是用于從state中獲取所需數(shù)據(jù)的函數(shù)妻率,通常在connect的第一個參數(shù) mapStateToProps中使用。例如板祝,我們在AuthorContainer.js中根據(jù)作者id獲取作者詳情信息宫静,不使用selector的話,可以這么寫:
//文件名:AuthorContainer.js
//省略無關代碼
function mapStateToProps(state, props) {
return {
author: state.authors[props.authorId],
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AuthorContainer);
這個例子中扔字,因為邏輯很簡單,直接獲取author看起來沒什么問題温技,但當獲取狀態(tài)的邏輯變得復雜時革为,需要通過一個函數(shù)來獲取,這個函數(shù)就是一個selector舵鳞。selector是可以復用的震檩,不同的容器組件,只要獲取狀態(tài)的邏輯相同蜓堕,就可以復用同樣的selector抛虏。所以,selector不能直接定義在某個容器組件中套才,而應該定義在其關聯(lián)領域所在的模塊中迂猴,這個例子需要定義在authors.js中。
//authors.js
//action types
//action creators
// reducer
// selectors
export function getAuthorById(state, id) {
return state[id]
}
在AuthorContainer.js中使用selector:
//文件名:AuthorContainer.js
import { getAuthorById } from '../redux/authors';
//省略無關代碼
function mapStateToProps(state, props) {
return {
author: getAuthorById(state.authors, props.authorId),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AuthorContainer);
我們再來看一個復雜些的selector:獲取一篇博客的評論列表背伴。獲取評論列表數(shù)據(jù)沸毁,需要posts和comments兩個領域的數(shù)據(jù)峰髓,所以這個selector并不適合放到comments.js模塊中。當一個selector的計算參數(shù)依賴多個狀態(tài)時息尺,可以把這個selector放到index.js中携兵,我們把index.js看做所有模塊層級之上的一個根模塊。
// index.js
// 省略無關代碼
// selectors
export function getCommentsByPost(post, comments) {
const commentIds = post.comments;
return commentIds.map(id => comments[id]);
}
我們在第2篇 如何合理地設計Redux的State講過搂誉,要像設計數(shù)據(jù)庫一樣設計state徐紧,selector就相當于查詢表的sql語句,reducer相當于修改表的sql語句炭懊。所以并级,本篇的總結(jié)是:像寫sql一樣,設計和組織action凛虽、reducer死遭、selector。
歡迎關注我的公眾號:老干部的大前端凯旋,領取21本大前端精選書籍呀潭!