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

时间:2016-07-18 19:57:13

标签: javascript redux spread-syntax

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键入的对象,或者是否有其他无法预料的痛点我将通过使用一系列对象来解决这个问题,这样我就应该中止该计划并尝试坚持使用键入的对象通过ID?

3 个答案:

答案 0 :(得分:36)

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

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

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

答案 1 :(得分:10)

  

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

这是关键的想法。

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

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

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

  • 不断重新创建代码
  • 将不必要的信息传递给减刑者和行动
  • 作为其中的后果,不好,不干净,不可扩展的代码。

我最终改变了我的数据结构并重写了很多代码。 您已被警告,请不要让自己陷入困境。

另外:

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

答案 2 :(得分:7)

  

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

您希望将实体保存在以ID作为键存储的对象中的主要原因(也称为规范化),使用深层嵌套对象真的很麻烦(这是您通常从更复杂的应用程序中的REST API获得的内容) - 适用于您的组件和缩减器。

使用您当前的示例来说明标准化状态的好处有点困难(因为您没有深层嵌套的结构)。但是,让我们说选项(在您的示例中)也有一个标题,并由系统中的用户创建。这将使响应看起来像这样:

[{
  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)看起来像id-key-by-id方法使得它变得更难   处理API的标准JSON输入/输出。 (这就是为什么我和他一起去的原因   首先是对象数组。)所以,如果你采用这种方法,   你只是使用一个函数来在JSON之间来回转换它   格式和状态形状格式?这看起来很笨拙。 (虽然如果你   倡导这种方法,是你的推理的一部分,而不是那样   比上面的数组对象减速器笨重?)

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

  3)我知道Dan Abramov在理论上设计了redux   状态数据结构不可知(如#34所建议的那样;按惯例,   顶级状态是一个对象或一些其他键值集合,如   地图,但从技术上讲,它可以是任何类型,"强调我的)。但是给了   以上,是否只是推荐"保持它是由ID键入的对象,   还是有其他不可预见的痛点我会遇到   使用一个对象数组,使我只能中止   计划并尝试坚持使用ID键入的对象?

这可能是对更复杂的应用程序的推荐,其中包含许多深度嵌套的API响应。但是,在你的特定例子中,它并不重要。