使用react构建大型应用,势必会面临状态管理的问题,redux是经常使用的一种状态管理库,咱们会由于各类缘由而须要使用它。javascript
但并非全部的state都要交给redux管理,当某个状态数据只被一个组件依赖或影响,且在切换路由再次返回到当前页面不须要保留操做状态时,咱们是没有必要使用redux的,用组件内部state足以。例以下拉框的显示与关闭。html
react应用中咱们会定义不少state,state最终也都是为页面展现服务的,根据数据的来源、影响的范围大体能够将前端state归为如下三类:前端
Domain data: 通常能够理解为从服务器端获取的数据,好比帖子列表数据、评论数据等。它们可能被应用的多个地方用到,前端须要关注的是与后端的数据同步、提交等等。UI state: 决定当前UI如何展现的状态,好比一个弹窗的开闭,下拉菜单是否打开,每每聚焦于某个组件内部,状态之间能够相互独立,也可能多个状态共同决定一个UI展现,这也是UI state管理的难点。java
App state: App级的状态,例如当前是否有请求正在loading、某个联系人被选中、当前的路由信息等可能被多个组件共同使用到状态。react
在使用redux的过程当中,咱们都会使用modules的方式,将咱们的reducers拆分到不一样的文件当中,一般会遵循高内聚、方便使用的原则,按某个功能模块、页面来划分。那对于某个reducer文件,如何设计state结构能更方便咱们管理数据呢,下面列出几种常见的方式:git
这种方式大多会出如今列表的展现上,如帖子列表页,由于后台接口返回的数据一般与列表的展现结构基本一致,能够直接使用。github
以下面的页面,分为三个section,对应开户中、即将流失、已提交审核三种不一样的数据类型。
由于页面是展现性的没有太多的交互,因此咱们彻底能够根据页面UI来设计以下的结构:web
tabData: { opening: [{ userId: "6332", mobile: "1858849****", name: "test1", ... }, ...], missing: [], commit: [{ userId: "6333", mobile: "1858849****", name: "test2", ... }, ... ] }
这样设计比较方便咱们将state映射到页面,拉取更多数据只须要将新数据简单contact进对应的数组便可。对于简单页面,这样是可行的。数据库
不少状况下,处理的数据都是嵌套或互相关联的。例如,一个群列表,由不少群组成,每一个群又包含不少个用户,一个用户能够加入多个不一样的群。这种类型的数据,咱们能够方便用以下结构表示:redux
const Groups = [ { id: 'group1', groupName: '连线电商', groupMembers: [ { id: 'user1', name: '张三', dept: '电商部' }, { id: 'user2', name: '李四', dept: '电商部' }, ] }, { id: 'group2', groupName: '连线资管', groupMembers: [ { id: 'user1', name: '张三', dept: '电商部' }, { id: 'user3', name: '王五', dept: '电商部' }, ] } ]
这种方式,对界面展现很友好,展现群列表,咱们只需遍历Groups数组,展现某个群成员列表,只需遍历相应索引的数据Groups[index],展现某个群成员的数据,继续索引到对应的成员数据GroupsgroupIndex便可。
可是这种方式有一些问题:
为了不上面的问题,咱们能够借鉴数据库存储数据的方式,设计出相似的范式化的state,范式化的数据遵循下面几个原则:
- 不一样类型的数据,都以“数据表”的形式存储在state中
- “数据表” 中的每一项条目都以对象的形式存储,对象以惟一性的ID做为key,条目自己做为value。
- 任何对单个条目的引用都应该根据存储条目的 ID 来索引完成。
- 数据的顺序经过ID数组表示。
上面的示例范式化以后以下:
{ groups: { byIds: { group1: { id: 'group1', groupName: '连线电商', groupMembers: ['user1', 'user2'] }, group2: { id: 'group2', groupName: '连线资管', groupMembers: ['user1', 'user3'] } }, allIds: ['group1', 'group2'] }, members: { byIds: { user1: { id: 'user1', name: '张三', dept: '电商部' }, user2: { id: 'user2', name: '李四', dept: '电商部' }, user3: { id: 'user3', name: '王五', dept: '电商部' } }, allIds: [] } }
与原来的数据相比有以下改进:
一般咱们接口返回的数据都是嵌套形式的,要将数据范式化,咱们可使用Normalizr这个库来辅助。
固然这样作以前咱们最好问本身,我是否须要频繁的遍历数据,是否须要快速的访问某一项数据,是否须要频繁更新同步数据。
对于这些关系数据,咱们能够统一放到entities中进行管理,这样root state,看起来像这样:
{ simpleDomainData1: {....}, simpleDomainData2: {....} entities : { entityType1 : {byId: {}, allIds}, entityType2 : {....} } ui : { uiSection1 : {....}, uiSection2 : {....} } }
其实上面的entities并不够纯粹,由于其中包含了关联关系(group里面包含了groupMembers的信息),也包含了列表的顺序信息(如每一个实体的allIds属性)。更进一步,咱们能够将这些信息剥离出来,让咱们的entities更加简单,扁平。
{ entities: { groups: { group1: { id: 'group1', groupName: '连线电商', }, group2: { id: 'group2', groupName: '连线资管', } }, members: { user1: { id: 'user1', name: '张三', dept: '电商部' }, user2: { id: 'user2', name: '李四', dept: '电商部' }, user3: { id: 'user3', name: '王五', dept: '电商部' } } }, groups: { gourpIds: ['group1', 'group2'], groupMembers: { group1: ['user1', 'user2'], group2: ['user2', 'user3'] } } }
这样咱们在更新entity信息的时候,只需操做对应entity就能够了,添加新的entity时则须要在对应的对象如entities[group]中添加group对象,在groups[groupIds]中添加对应的关联关系。
enetities.js
const ADD_GROUP = 'entities/addGroup'; const UPDATE_GROUP = 'entities/updateGroup'; const ADD_MEMBER = 'entites/addMember'; const UPDATE_MEMBER = 'entites/updateMember'; export const addGroup = entity => ({ type: ADD_GROUP, payload: {[entity.id]: entity} }) export const updateGroup = entity => ({ type: UPDATE_GROUP, payload: {[entity.id]: entity} }) export const addMember = member => ({ type: ADD_MEMBER, payload: {[member.id]: member} }) export const updateMember = member => ({ type: UPDATE_MEMBER, payload: {[member.id]: member} }) _addGroup(state, action) { return state.set('groups', state.groups.merge(action.payload)); } _addMember(state, action) { return state.set('members', state.members.merge(action.payload)); } _updateGroup(state, action) { return state.set('groups', state.groups.merge(action.payload, {deep: true})); } _updateMember(state, action) { return state.set('members', state.members.merge(action.payload, {deep: true})) } const initialState = Immutable({ groups: {}, members: {} }) export default function entities(state = initialState, action) { let type = action.type; switch (type) { case ADD_GROUP: return _addGroup(state, action); case UPDATE_GROUP: return _updateGroup(state, action); case ADD_MEMBER: return _addMember(state, action); case UPDATE_MEMBER: return _updateMember(state, action); default: return state; } }
能够看到,由于entity的结构大体相同,因此更新起来不少逻辑是差很少的,因此这里能够进一步提取公用函数,在payload里面加入要更新的key值。
export const addGroup = entity => ({ type: ADD_GROUP, payload: {data: {[entity.id]: entity}, key: 'groups'} }) export const updateGroup = entity => ({ type: UPDATE_GROUP, payload: {data: {[entity.id]: entity}, key: 'groups'} }) export const addMember = member => ({ type: ADD_MEMBER, payload: {data: {[member.id]: member}, key: 'members'} }) export const updateMember = member => ({ type: UPDATE_MEMBER, payload: {data: {[member.id]: member}, key: 'members'} }) function normalAddReducer(state, action) { let payload = action.payload; if (payload && payload.key) { let {key, data} = payload; return state.set(key, state[key].merge(data)); } return state; } function normalUpdateReducer(state, action) { if (payload && payload.key) { let {key, data} = payload; return state.set(key, state[key].merge(data, {deep: true})); } } export default function entities(state = initialState, action) { let type = action.type; switch (type) { case ADD_GROUP: case ADD_MEMBER: return normalAddReducer(state, action); case UPDATE_GROUP: case UPDATE_MEMBER: return normalUpdateReducer(state, action); default: return state; } }
在请求接口时,一般会dispatch loading状态,一般咱们会在某个接口请求的reducer里面来处理响应的loading状态,这会使loading逻辑处处都是。其实咱们能够将loading状态做为根reducer的一部分,单独管理,这样就能够复用响应的逻辑。
const SET_LOADING = 'SET_LOADING'; export const LOADINGMAP = { groupsLoading: 'groupsLoading', memberLoading: 'memberLoading' } const initialLoadingState = Immutable({ [LOADINGMAP.groupsLoading]: false, [LOADINGMAP.memberLoading]: false, }); const loadingReducer = (state = initialLoadingState, action) => { const { type, payload } = action; if (type === SET_LOADING) { return state.set(key, payload.loading); } else { return state; } } const setLoading = (scope, loading) => { return { type: SET_LOADING, payload: { key: scope, loading, }, }; } // 使用的时候 store.dispatch(setLoading(LOADINGMAP.groupsLoading, true));
这样当须要添加新的loading状态的时候,只须要在LOADINGMAP和initialLoadingState添加相应的loading type便可。
也能够参考dva的实现方式,它也是将loading存储在根reducer,而且是根据model的namespace做为区分,
它方便的地方在于将更新loading状态的逻辑被提取到plugin中,用户不须要手动编写更新loading的逻辑,只须要在用到时候使用state便可。plugin的代码也很简单,就是在钩子函数中拦截反作用。
function onEffect(effect, { put }, model, actionType) { const { namespace } = model; return function*(...args) { yield put({ type: SHOW, payload: { namespace, actionType } }); yield effect(...args); yield put({ type: HIDE, payload: { namespace, actionType } }); }; }
对于web端应用,咱们没法控制用户的操做路径,极可能用户在直接访问某个页面的时候,咱们store中并无准备好数据,这可能会致使一些问题,因此有人建议以page为单位划分store,舍弃掉部分多页面共享state的好处,具体能够参考这篇文章,其中提到在视图之间共享state要谨慎,其实这也反映出咱们在思考是否要共享某个state时,思考以下几个问题:
https://www.zhihu.com/questio...
https://segmentfault.com/a/11...
https://hackernoon.com/shape-...
https://medium.com/@dan_abram...
https://medium.com/@fastphras...
https://juejin.im/post/59a16e...
http://cn.redux.js.org/docs/r...
https://redux.js.org/recipes/...