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

State as array of objects vs object keyed by id

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

Keep every entity in an object stored with an ID as a key, and use IDs to reference it from other entities, or lists.

他们接着说

Think of the app’s state as a database.

我正在处理过滤器列表的状态形状,其中一些将打开(它们显示在弹出窗口中),或已选择选项。当我阅读 "Think of the app’s state as a database," 时,我考虑将它们视为 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").

应该没关系

所以我愉快地使用了对象数组方法,直到我编写了我的 reducer。

使用 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) reducer 的简单性是否是采用 object-keyed-by-id 方法的动机?该状态形状还有其他优点吗?

2) 对象按 ID 键控的方法似乎更难处理 API 的标准 JSON in/out。 (这就是我首先使用对象数组的原因。)所以如果你使用这种方法,你是否只使用一个函数在 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," 强调我的建议)。但是考虑到上述情况,是否只是 "recommended" 让它成为一个由 ID 键控的对象,或者是否有其他无法预料的痛点我将 运行 通过使用一个对象数组来使它成为这样我应该放弃那个计划并尝试坚持使用 ID 键控的对象吗?

Think of the app’s state as a database.

这是关键思想。

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

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

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

  • 在整个代码中不断重新创建对象
  • 将不必要的信息传递给 reducer 和 actions
  • 因此,糟糕、不干净且不可扩展的代码。

我最终改变了我的数据结构并重写了很多代码。 已警告您,请不要自找麻烦。

另外:

4) 大多数带有 ID 的集合旨在使用 ID 作为对整个对象的引用,您应该利用它。 API 调用将获得 ID ,然后是 其余参数,您的操作和 reducers 也将如此。

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

Q2:您可以使用normalizr来帮助您从API转换为存储。从 normalizr V3.1.0 开始,您可以使用 denormalize 走另一条路。也就是说,应用程序通常是消费者而不是数据生产者,因此通常更频繁地转换为存储。

Q3:使用数组 运行 遇到的问题与其说是存储约定 and/or 不兼容问题,不如说是性能问题。

1) Is the simplicity of the reducer the motivation for going with the object-keyed-by-id approach? Are there other advantages to that state shape?

您希望将 objects 中的实体以 ID 作为键存储(也称为 规范化 )的主要原因是使用 深度嵌套 objects(这是您通常在更复杂的应用程序中从 REST APIs 获得的内容)— 既适用于您的组件,也适用于您的 reducer。

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

[{
  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) It seems like the object-keyed-by-id approach makes it harder to deal with standard JSON in/out for an API. (That's why I went with the array of objects in the first place.) So if you go with that approach, do you just use a function to transform it back and forth between JSON format and state shape format? That seems clunky. (Though if you advocate that approach, is part of your reasoning that that's less clunky than the array-of-objects reducer above?)

A JSON-response 可以使用例如规范化normalizr.

3) I know Dan Abramov designed redux to theoretically be state-data-structure agnostic (as suggested by "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," emphasis mine). But given the above, is it just "recommended" to keep it an object keyed by ID, or are there other unforeseen pain points I'm going to run into by using an array of objects that make it such that I should just abort that plan and try to stick with an object keyed by ID?

这可能是对具有大量深度嵌套 API 响应的更复杂应用程序的建议。不过,在您的特定示例中,这并不重要。