首页 文章

redux:作为对象数组的状态与由id键入的对象的状态

提问于
浏览
69

Designing the State Shape章节中,文档建议将您的状态保存在由ID键入的对象中:

将每个实体保存在以ID作为密钥存储的对象中,并使用ID从其他实体或列表中引用它 .

他们继续陈述

将应用程序的状态视为数据库 .

我正在处理状态形状以获取过滤器列表,其中一些将打开(它们显示在弹出窗口中),或者已选择选项 . 当我读到“将应用程序的状态视为数据库”时,我考虑将它们视为JSON响应,因为它将从API(本身由数据库支持)返回 .

所以我一直在考虑它

[{
    id: '1',
    name: 'View',
    open: false,
    options: ['10', '11', '12', '13'],
    selectedOption: ['10'],
    parent: null,
  },
  {
    id: '10',
    name: 'Time & Fees',
    open: false,
    options: ['20', '21', '22', '23', '24'],
    selectedOption: null,
    parent: '1',
  }]

但是,文档建议更像一种格式

{
   1: { 
    name: 'View',
    open: false,
    options: ['10', '11', '12', '13'],
    selectedOption: ['10'],
    parent: null,
  },
  10: {
    name: 'Time & Fees',
    open: false,
    options: ['20', '21', '22', '23', '24'],
    selectedOption: null,
    parent: '1',
  }
}

从理论上讲,只要data is serializable (under the heading "State")就没关系 .

所以我愉快地使用了对象阵列方法,直到我写了我的减速器 .

使用object-keyed-by-id方法(以及扩展语法的自由使用),reducer的 OPEN_FILTER 部分变为

switch (action.type) {
  case OPEN_FILTER: {
    return { ...state, { ...state[action.id], open: true } }
  }

使用对象数组方法,它更冗长(和辅助函数依赖)

switch (action.type) {
   case OPEN_FILTER: {
      // relies on getFilterById helper function
      const filter = getFilterById(state, action.id);
      const index = state.indexOf(filter);
      return state
        .slice(0, index)
        .concat([{ ...filter, open: true }])
        .concat(state.slice(index + 1));
    }
    ...

所以我的问题有三个:

1)减速器的简单性是否采用对象键控id方法的动机?这种状态还有其他优点吗?

2)看起来像object-keyed-by-id方法使得处理API的标准JSON输入/输出变得更加困难 . (这就是我首先使用对象数组的原因 . )因此,如果你采用这种方法,你是否只使用一个函数在JSON格式和状态形状格式之间来回转换它?这看起来很笨拙 . (虽然如果你提倡这种方法,你的推理是否比上面的对象数组减少器更少笨重?)

3)我知道Dan Abramov设计的redux在理论上是状态数据结构不可知的(正如"By convention, the top-level state is an object or some other key-value collection like a Map, but technically it can be any type,"强调我的建议) . 但鉴于上述情况,将它作为ID键入的对象只是"recommended",还是还有其他无法预料的痛点我将通过使用一系列对象来解决这个问题,这样我就应该中止该计划,试着坚持使用ID键入的对象?

3 回答

  • 33

    Q1:reducer的简单性是不必搜索数组以找到正确的条目 . 不必搜索阵列是有利的 . 选择器和其他数据访问器可能并且经常通过 id 访问这些项目 . 必须在阵列中搜索每次访问都会成为性能问题 . 当阵列变大时,性能问题会急剧恶化 . 此外,随着您的应用变得更加复杂,在更多地方显示和过滤数据,问题也会恶化 . 这种组合可能是有害的 . 通过 id 访问项目,访问时间从 O(n) 变为 O(1) ,对于大 n (此处为数组项目),访问时间会产生巨大差异 .

    Q2:您可以使用normalizr来帮助您进行从API到商店的转换 . 从normalizr V3.1.0开始,你可以使用denormalize来反过来 . 也就是说,应用程序通常比数据 生产环境 者更多的消费者,因此转换到商店通常更频繁地完成 .

    问题3:使用数组遇到的问题不是存储约定和/或不兼容问题,而是更多的性能问题 .

  • 10

    将应用程序的状态视为数据库 .

    这是关键的想法 .

    1)具有唯一ID的对象允许您在引用对象时始终使用该id,因此您必须在actions和reducers之间传递最小数量的数据 . 它比使用array.find(...)更有效 . 如果使用数组方法,则必须传递整个对象并且很快就会变得混乱,最终可能会在不同的reducers,actions或甚至容器中重新创建对象(您不希望这样) . 视图将始终能够获取完整对象,即使它们的关联reducer仅包含ID,因为在映射状态时,您将获得集合(视图获取整个状态以将其映射到属性) . 由于我所说的所有内容,操作最终只有最小量的参数,并且减少了最小的信息量,试一试,尝试这两种方法,你会发现架构最终更具可扩展性和清洁性如果集合具有ID,则为ID .

    2)与API的连接不应影响存储和缩减器的体系结构,这就是原因你有行动,保持关注点的分离 . 只需将转换逻辑放入和放出API中的可重用模块,在使用API的操作中导入该模块,应该是它 .

    3)我使用带有ID的结构的数组,这是我所遭受的不可预见的后果:

    • 重新创建对象,使代码变得更加严格

    • 将必要信息传递给减速器和动作

    • 作为其中的后果,坏,不干净,不可扩展的代码 .

    我最终改变了我的数据结构并重写了很多代码 . You have been warned, please don't get yourself in trouble.

    也:

    4)大多数带ID的集合都是使用ID作为整个对象的引用,你应该利用它 . API调用将获得ID and then 其余参数,因此您的操作和缩减器也将如此 .

  • 6

    1)减速器的简单性是否采用对象键控id方法的动机?这种状态还有其他优点吗?

    您希望将实体保存在以ID作为键存储的对象(也称为 normalized )中的主要原因是,使用 deeply nested objects (这是您通常从更复杂的应用程序中的REST API获取的内容)非常麻烦 - 两者都是你的组件和减速器 .

    使用当前示例(如 you don't have a deeply nested structure )来说明规范化状态的好处有点困难 . 但是,让我们说选项(在您的示例中)也有一个 Headers ,并由系统中的用户创建 . 这会使响应看起来像这样:

    [{
      id: 1,
      name: 'View',
      open: false,
      options: [
        {
          id: 10, 
          title: 'Option 10',
          created_by: { 
            id: 1, 
            username: 'thierry' 
          }
        },
        {
          id: 11, 
          title: 'Option 11',
          created_by: { 
            id: 2, 
            username: 'dennis'
          }
        },
        ...
      ],
      selectedOption: ['10'],
      parent: null,
    },
    ...
    ]
    

    现在假设您要创建一个组件,该组件显示已创建选项的所有用户的列表 . 要做到这一点,首先必须请求所有项目,然后遍历每个选项,最后获取created_by.username .

    更好的解决方案是将响应规范化为:

    results: [1],
    entities: {
      filterItems: {
        1: {
          id: 1,
          name: 'View',
          open: false,
          options: [10, 11],
          selectedOption: [10],
          parent: null
        }
      },
      options: {
        10: {
          id: 10,
          title: 'Option 10',
          created_by: 1
        },
        11: {
          id: 11,
          title: 'Option 11',
          created_by: 2
        }
      },
      optionCreators: {
        1: {
          id: 1,
          username: 'thierry',
        },
        2: {
          id: 2,
          username: 'dennis'
        }
      }
    }
    

    使用这种结构,列出已创建选项的所有用户(我们将它们隔离在entities.optionCreators中,因此我们只需循环遍历该列表)就更容易,也更有效 .

    显示例如也很简单已为ID为1的过滤器项创建选项的用户名:

    entities
      .filterItems[1].options
      .map(id => entities.options[id])
      .map(option => entities.optionCreators[option.created_by].username)
    

    2)似乎object-keyed-by-id方法使得处理API的标准JSON输入/输出变得更加困难 . (这就是我首先使用对象数组的原因 . )因此,如果你采用这种方法,你是否只使用一个函数在JSON格式和状态形状格式之间来回转换它?这看起来很笨拙 . (虽然如果你提倡这种方法,你的推理是否比上面的对象数组减少器更少笨重?)

    可以使用例如JSON响应来标准化JSON响应 . normalizr .

    3)我知道Dan Abramov将redux设计为理论上是状态数据结构不可知的(正如“按惯例所示,顶级状态是一个对象或一些其他键值集合,如Map,但从技术上讲,它可以是任何类型,“强调我的” . 但鉴于上述情况,是否只是“建议”将其保持为ID键入的对象,或者是否存在其他无法预料的痛点,我将通过使用一系列对象来解决这个问题,这样我就应该中止计划并尝试坚持使用ID键入的对象?

    这可能是对具有大量嵌套API响应的更复杂应用的推荐 . 但是在你的特定例子中,它并不重要 .

相关问题