资讯专栏INFORMATION COLUMN

如何设计redux state结构

huangjinnan / 3096人阅读

摘要:例如下拉框的显示与关闭。如何设计结构在使用的过程中,我们都会使用的方式,将我们的拆分到不同的文件当中,通常会遵循高内聚方便使用的原则,按某个功能模块页面来划分。

为什么使用redux

使用react构建大型应用,势必会面临状态管理的问题,redux是常用的一种状态管理库,我们会因为各种原因而需要使用它。

不同的组件可能会使用相同的数据,使用redux能更好的复用数据和保持数据的同步

react中子组件访问父组件的数据只能通过props层层传递,使用redux可以轻松的访问到想要的数据

全局的state可以很容易的进行数据持久化,方便下次启动app时获得初始state

dev tools提供状态快照回溯的功能,方便问题的排查

但并不是所有的state都要交给redux管理,当某个状态数据只被一个组件依赖或影响,且在切换路由再次返回到当前页面不需要保留操作状态时,我们是没有必要使用redux的,用组件内部state足以。例如下拉框的显示与关闭。

常见的状态类型

react应用中我们会定义很多state,state最终也都是为页面展示服务的,根据数据的来源、影响的范围大致可以将前端state归为以下三类:

Domain data: 一般可以理解为从服务器端获取的数据,比如帖子列表数据、评论数据等。它们可能被应用的多个地方用到,前端需要关注的是与后端的数据同步、提交等等。

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范式化(normalize)

很多情况下,处理的数据都是嵌套或互相关联的。例如,一个群列表,由很多群组成,每个群又包含很多个用户,一个用户可以加入多个不同的群。这种类型的数据,我们可以方便用如下结构表示:

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状态的逻辑被提取到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/...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/109428.html

相关文章

  • Redux进阶系列2: 如何合理地设计ReduxState

    摘要:设计一个好的并非易事,本文先从设计时最容易犯的两个错误开始介绍,然后引出如何合理地设计。错误以为设计的依据以为设计的依据,往往是一个对应一个子,的结构同返回的数据结构保持一致或接近一致。至此,的结构设计完成。 Redux是一个非常流行的状态管理解决方案,Redux应用执行过程中的任何一个时刻,都是一个状态的反映。可以说,State 驱动了Redux逻辑的运转。设计一个好的State并非...

    刘明 评论0 收藏0
  • Redux概念之二: Redux的三大原则

    摘要:就是应用程序领域的状态,它是类型中的模型的设计的概念,这设计是由架构而来的,在原本的架构中是允许多个的结构,简化为只有单一个。的设计中是与中的相比,它们之间有一些类似的设计。 Redux里的强硬规则与设计不少,大部份都会与FP(函数式程序开发)、改进原本的Flux架构设计有关。Redux官网文档上的三大基本原则,主要是因为有可能怕初学者不理解Redux中的一些限制或设计,所以先写出来说...

    dingda 评论0 收藏0
  • 干货 | React技术栈耕耘 —— Redux

    摘要:作者小沪江前端开发工程师本文为原创文章,有不当之处欢迎指出。于是,单一数据源规则实施起来,是规定用的顶层容器组件的来存储单一对象树,同时交给来管理。顾名思义,当更新时,的回调函数会更新视图层,以达到订阅的效果。 作者:小boy (沪江web前端开发工程师)本文为原创文章,有不当之处欢迎指出。转载请注明出处。文章示例代码:https://github.com/ikcamp/rea... ...

    LdhAndroid 评论0 收藏0
  • 简析React 和 Redux 的特点和关系

    摘要:这对复杂问题定位是有好处的。同时,也是纯函数,与的是纯函数呼应。强约束约定,增加了内聚合性。通过约定和全局的理解,可以减少的一些缺点。约定大于配置也是框架的主要发展方向。 React+Redux非常精炼,良好运用将发挥出极强劲的生产力。但最大的挑战来自于函数式编程(FP)范式。在工程化过程中,架构(顶层)设计将是一个巨大的挑战。要不然做出来的东西可能是一团乱麻。说到底,传统框架与rea...

    iOS122 评论0 收藏0
  • Vuex — The core of Vue application

    摘要:个人看来,一个状态管理的应用,无论是使用,还是,最困难的部分是在的设计。中,并没有移除,而是改为用于触发。也是一个对象,用于注册,每个都是一个用于返回一部分的。接受一个数组或对象,根据相应的值将对应的绑定到组件上。 系列文章: Vue 2.0 升(cai)级(keng)之旅 Vuex — The core of Vue application (本文) 从单页应用(SPA)到服务器...

    Aldous 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<