Redux進階系列3:如何設計action看铆、reducer挤巡、selector

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本大前端精選書籍呀潭!

image
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市至非,隨后出現(xiàn)的幾起案子钠署,更是在濱河造成了極大的恐慌,老刑警劉巖荒椭,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谐鼎,死亡現(xiàn)場離奇詭異,居然都是意外死亡趣惠,警方通過查閱死者的電腦和手機狸棍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來味悄,“玉大人草戈,你說我怎么就攤上這事∈躺” “怎么了唐片?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長涨颜。 經(jīng)常有香客問我费韭,道長,這世上最難降的妖魔是什么庭瑰? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任星持,我火速辦了婚禮,結(jié)果婚禮上弹灭,老公的妹妹穿的比我還像新娘钉汗。我一直安慰自己羹令,他們只是感情好,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布损痰。 她就那樣靜靜地躺著福侈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪卢未。 梳的紋絲不亂的頭發(fā)上肪凛,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音辽社,去河邊找鬼伟墙。 笑死,一個胖子當著我的面吹牛滴铅,可吹牛的內(nèi)容都是我干的戳葵。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼汉匙,長吁一口氣:“原來是場噩夢啊……” “哼拱烁!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起噩翠,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤戏自,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后伤锚,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體擅笔,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年屯援,在試婚紗的時候發(fā)現(xiàn)自己被綠了猛们。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡狞洋,死狀恐怖弯淘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情徘铝,我是刑警寧澤耳胎,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布惯吕,位于F島的核電站惕它,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏废登。R本人自食惡果不足惜淹魄,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望堡距。 院中可真熱鬧甲锡,春花似錦兆蕉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至缸废,卻和暖如春包蓝,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背企量。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工测萎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人届巩。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓硅瞧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親恕汇。 傳聞我的和親對象是個殘疾皇子腕唧,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 學習必備要點: 首先弄明白,Redux在使用React開發(fā)應用時拇勃,起到什么作用——狀態(tài)集中管理 弄清楚Redux是...
    賀賀v5閱讀 8,896評論 10 58
  • http://gaearon.github.io/redux/index.html 四苇,文檔在 http://rac...
    jacobbubu閱讀 79,951評論 35 198
  • Redux對于React程序是可有可無的嗎?當你認識到Redux在編程時給你那種可以掌控一切狀態(tài)能力的時候,你會覺...
    smartphp閱讀 957評論 0 5
  • 看到這篇文章build an image gallery using redux saga,覺得寫的不錯方咆,長短也適...
    smartphp閱讀 6,154評論 1 29
  • 疾馳的車輛 寧靜的街角 淡黃的燈光 鉛灰的夜空 行人走在夜色里月腋,都有獨屬自己內(nèi)心的故事
    午后先生閱讀 182評論 0 1