内容简介:使用react构建大型应用,势必会面临状态管理的问题,redux是常用的一种状态管理库,我们会因为各种原因而需要使用它。但并不是所有的state都要交给redux管理,当某个状态数据只被一个组件依赖或影响,且在切换路由再次返回到当前页面不需要保留操作状态时,我们是没有必要使用redux的,用组件内部state足以。例如下拉框的显示与关闭。react应用中我们会定义很多state,state最终也都是为页面展示服务的,根据数据的来源、影响的范围大致可以将前端state归为以下三类:
为什么使用redux
使用react构建大型应用,势必会面临状态管理的问题,redux是常用的一种状态管理库,我们会因为各种原因而需要使用它。
- 不同的组件可能会使用相同的数据,使用redux能更好的复用数据和保持数据的同步
- react中子组件访问父组件的数据只能通过props层层传递,使用redux可以轻松的访问到想要的数据
- 全局的state可以很容易的进行数据持久化,方便下次启动app时获得初始state
- dev tools提供状态快照回溯的功能,方便问题的排查
但并不是所有的state都要交给redux管理,当某个状态数据只被一个组件依赖或影响,且在切换路由再次返回到当前页面不需要保留操作状态时,我们是没有必要使用redux的,用组件内部state足以。例如下拉框的显示与关闭。
常见的状态类型
react应用中我们会定义很多state,state最终也都是为页面展示服务的,根据数据的来源、影响的范围大致可以将前端state归为以下三类:
UI state: 决定当前UI如何展示的状态,比如一个弹窗的开闭,下拉菜单是否打开,往往聚焦于某个组件内部,状态之间可以相互独立,也可能多个状态共同决定一个UI展示,这也是UI state管理的难点。
App state: App级的状态,例如当前是否有请求正在loading、某个联系人被选中、当前的路由信息等可能被多个组件共同使用到状态。
如何设计state结构
在使用redux的过程中,我们都会使用modules的方式,将我们的reducers拆分到不同的文件当中,通常会遵循高内聚、方便使用的原则,按某个功能模块、页面来划分。那对于某个reducer文件,如何设计state结构能更方便我们管理数据呢,下面列出几种常见的方式:
1.将api返回的数据直接放入state
这种方式大多会出现在列表的展示上,如帖子列表页,因为后台接口返回的数据通常与列表的展示结构基本一致,可以直接使用。
2.以页面UI来设计state结构
如下面的页面,分为三个section,对应开户中、即将流失、已提交审核三种不同的数据类型。
因为页面是展示性的没有太多的交互,所以我们完全可以根据页面UI来设计如下的结构:
tabData: {
opening: [{
userId: "6332",
mobile: "1858849****",
name: "test1",
...
}, ...],
missing: [],
commit: [{
userId: "6333",
mobile: "1858849****",
name: "test2",
...
}, ... ]
}
这样设计比较方便我们将state映射到页面,拉取更多数据,也只简单contact进对应的数组即可。对于简单页面,这样是可行的。
3.State范式化(normailize)
很多情况下,处理的数据都是嵌套或互相关联的。例如,一个群列表,由很多群组成,每个群又包含很多个用户,一个用户可以加入多个不同的群。这种类型的数据,我们可以方便用如下结构表示:
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即可。
但是这种方式有一些问题:
- 存在很多重复数据,当某个群成员信息更新的时候,想要在不同的群之间进行同步比较麻烦。
- 嵌套过深,导致reducer逻辑复杂,修改深层的属性会导致代码臃肿,空指针的问题
- redux中需要遵循 不可变更新模式 ,更新属性往往需要更新组件树的祖先,产生新的引用,这会导致跟修改数据无关的组件也要重新render。
为了避免上面的问题,我们可以借鉴数据库存储数据的方式,设计出类似的范式化的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: []
}
}
与原来的数据相比有如下改进:
- 因为数据是扁平的,且只被定义在一个地方,更方便数据更新
- 检索或者更新给定数据项的逻辑变得简单与一致。给定一个数据项的 type 和 ID,不必嵌套引用其他对象而是通过几个简单的步骤就能查找到它。
- 每个数据类型都是唯一的,像用户信息这样的更新仅仅需要状态树中 “members > byId > user” 这部分的复制。这也就意味着在 UI 中只有数据发生变化的一部分才会发生更新。与之前的不同的是,之前嵌套形式的结构需要更新整个 groupMembers数组,以及整个 groups数组。这样就会让不必要的组件也再次重新渲染。
通常我们接口返回的数据都是嵌套形式的,要将数据范式化,我们可以使用 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;
}
}
将loading状态抽离到根reducer中,统一管理
在请求接口时,通常会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状态的逻辑自动化,用户不需要手动更新loading,只需要在用到时候使用state即可,更高级。
其他
对于web端应用,我们无法控制用户的操作路径,很可能用户在直接访问某个页面的时候,我们store中并没有准备好数据,这可能会导致一些问题,所以有人建议以page为单位划分store,舍弃掉部分多页面共享state的好处,具体可以参考这篇文章,其中提到在视图之间共享state要谨慎,其实这也反映出我们在思考是否要共享某个state时,思考如下几个问题:
- 有多少页面会使用到该数据
- 每个页面是否需要单独的数据副本
- 改动数据的频率怎么样
参考文章
以上所述就是小编给大家介绍的《如何设计redux state结构》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 设计模式 结构型模式
- 基于Web的svg编辑器(2)——层次结构设计(DOM结构)
- C语言程序设计——控制结构(顺序)
- 打破认知:程序设计 = 算法 + 数据结构?
- Go项目结构设计过程点滴记录
- 结构型设计模式之桥接模式
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Beautiful Code
Greg Wilson、Andy Oram / O'Reilly Media / 2007-7-6 / GBP 35.99
In this unique work, leading computer scientists discuss how they found unusual, carefully designed solutions to difficult problems. This book lets the reader look over the shoulder of major coding an......一起来看看 《Beautiful Code》 这本书的介绍吧!