状态表示为对象数组vs ID键入的对象


94

在“ 设计状态形状 ”一章中,文档建议将状态保留在以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',
  }
}

从理论上讲,只要数据可序列化(在“状态”标题下)就没有关系

因此,我快乐地使用了对象数组方法,直到编写减速器为止。

使用对象按ID的方法(以及对扩展语法的自由使用),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)减速器的简单性是采用对象按键输入方法的动力吗?该状态形状还有其他优点吗?

2)似乎ID的对象键输入方法使处理API的标准JSON输入/输出变得更加困难。(这就是为什么我首先使用对象数组的原因。)因此,如果您采用这种方法,是否只是使用一个函数在JSON格式和状态形状格式之间来回转换呢?看起来笨拙。(尽管您提倡使用这种方法,但您的推理的一部分是否比上面的对象数组化简器笨拙?)

3)我知道Dan Abramov将Redux设计为在理论上与状态数据结构无关(如“按惯例建议,顶级状态是对象或其他一些键值集合,例如Map,但从技术上讲,它可以是任何类型)。但是,鉴于上述情况,是“推荐”将其保留为以ID为键的对象,还是通过使用一系列使它成为对象的对象而遇到其他无法预见的痛点,所以我应该中止该操作计划并尝试坚持使用以ID为键的对象?


2
这是一个有趣的问题,我也想提供一些见解,尽管我倾向于在redux而不是数组中进行归一化(纯粹是因为查找更容易),但我确实发现,如果采用归一化方法,排序将变得这是一个问题,因为您无法获得与数组相同的结构,因此您不得不对自己进行排序。
罗伯特·桑德斯

我看到“ id键入对象”方法存在问题,但是这种情况并不常见,但是在编写任何UI应用程序时我们都必须考虑这种情况。那么,如果我想使用作为有序列表列出的拖放元素来更改实体的顺序,该怎么办?通常,“按ID键入对象”方法在这里会失败,并且我肯定会使用对象方法数组来避免此类慷慨的问题。可能还有更多但想在这里共享的信息
Kunal Navhate

如何对由对象组成的对象进行排序?这似乎是不可能的。
David Vielhuber,

@DavidVielhuber您的意思是除了使用lodash之类的东西外sort_byconst sorted = _.sortBy(collection, 'attribute');
nickcoxdotme

是。当前,我们将这些对象转换为vue计算属性内部的数组
David Vielhuber

Answers:


46

问题1:Reducer的简单性是不必搜索数组即可找到正确的条目的结果。优点是不必搜索数组。选择器和其他数据访问器可能并且经常通过来访问这些项目id。必须在阵列中搜索每个访问权限成为性能问题。当阵列变大时,性能问题会急剧恶化。另外,随着您的应用程序变得越来越复杂,在更多位置显示和过滤数据,问题也变得更加严重。这种组合可能是有害的。通过访问项by id,访问时间从更改O(n)O(1),对于大型n项(此处为数组项),访问时间有很大差异。

问题2:您可以normalizr用来帮助您完成从API到商店的转换。从normalizr V3.1.0开始,您可以使用denormalize进行其他操作。也就是说,与数据生产者相比,应用通常是更多的消费者,因此通常更频繁地进行存储转换。

问题3:使用阵列会遇到的问题不是存储约定和/或不兼容问题,而是更多的性能问题。


一旦我们更改后端的defs,规范化工具肯定会带来痛苦。因此,每次都必须保持最新状态
Kunal Navhate

12

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

那是关键思想。

1)具有唯一ID的对象使您在引用对象时始终可以使用该ID,因此必须在操作和化简器之间传递最少的数据量。比使用array.find(...)更有效。如果使用数组方法,则必须传递整个对象,并且很快就会变得混乱,您可能最终会在不同的reducer,action甚至在容器中重新创建对象(您不希望这样)。即使视图的关联化约器仅包含ID,视图也将始终能够获取完整的对象,因为在映射状态时,您将在某个位置获得集合(视图将整个状态映射到属性)。由于我已经说了所有这些,所以操作最终将拥有最少的参数,而缩减器则拥有最少的信息,请尝试一下,

2)与API的连接不应影响您的存储和reducer的体系结构,这就是为什么您要采取措施保持关注点分离。只需将您的转换逻辑放入和退出API的可重用模块中,然后在使用该API的操作中导入该模块即可。

3)我将数组用于具有ID的结构,这是我遭受的无法预料的后果:

  • 在整个代码中不断重新创建对象
  • 将不必要的信息传递给减速器和动作
  • 结果,不良的,不干净的,不可扩展的代码。

我最终改变了数据结构,并重写了许多代码。您已收到警告,请不要惹麻烦。

也:

4)大多数具有ID的集合都打算将ID用作对整个对象的引用,您应该利用这一点。API调用将获取ID ,然后获取其余参数,您的操作和reducer 也将获取。


我遇到了一个问题,即我们有一个应用程序,该应用程序通过id将大量数据(1000到10,000个)存储在redux存储区的对象中。在视图中,它们都使用排序的数组来显示时间序列数据。这意味着每次重新渲染完成后,都必须获取整个对象,将其转换为数组,然后对其进行排序。我的任务是改善应用程序的性能。这是一种用例,在这种情况下,将数据存储在排序数组中并使用二进制搜索而不是对象进行删除和更新更有意义吗?
周五

我最终不得不制作一些其他从该数据派生的哈希映射,以最大程度地减少更新时的计算时间。这使得更新所有不同的视图需要它们自己的更新逻辑。在此之前,所有组件都将从存储中获取对象,并重建进行视图所需的数据结构。我可以想到的一种确保UI中最小程度混乱的方法是使用Web Worker进行从对象到数组的转换。折衷方案是更简单的检索和更新逻辑,因为所有组件仅依赖于要读写的一种数据类型。
威廉周杰伦

8

1)减速器的简单性是采用对象按键输入方法的动力吗?该状态形状还有其他优点吗?

您希望将实体保留在以ID作为键存储的对象中的主要原因(也称为normalized)是,使用深度嵌套的对象确实很麻烦(这通常是从更复杂的应用程序中的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的对象键输入方法使处理API的标准JSON输入/输出变得更加困难。(这就是为什么我首先使用对象数组的原因。)因此,如果您采用这种方法,是否只是使用一个函数在JSON格式和状态形状格式之间来回转换呢?看起来笨拙。(尽管您提倡使用这种方法,但您的推理的一部分是否比上面的对象数组化简器笨拙?)

JSON响应可以使用normalizr规范化

3)我知道Dan Abramov将Redux设计为在理论上与状态数据结构无关(如“按惯例建议,顶级状态是对象或其他一些键值集合,例如Map,但从技术上讲,它可以是任何类型”)。但是,鉴于上述情况,是“推荐”将其保留为以ID为键的对象,还是通过使用一系列使它成为对象的对象而遇到其他无法预见的痛点,所以我应该中止该操作计划并尝试坚持使用以ID为键的对象?

对于具有许多深层嵌套的API响应的更复杂的应用程序,这可能是一个建议。不过,在您的特定示例中,它并不重要。


1
map如果资源是分别获取的,则返回undefined,如here所示,这使得filter方法过于复杂。有解决方案吗?
Saravanabalagi Ramachandran

1
@tobiasandersen您认为服务器可以返回理想的用于react / redux的标准化数据,以避免客户端通过诸如normalizr的库进行转换吗?换句话说,使服务器而非客户端标准化数据。
马修(Matthew)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.