106

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',
  }
}

从理论上讲,只要数据是可序列化的(在“状态”标题下) ,这无关紧要。

所以我很高兴地使用了对象数组方法,直到我开始编写我的 reducer。

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

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) 似乎 object-keyed-by-id 方法使得处理 API 的标准 JSON 输入/输出变得更加困难。(这就是我首先使用对象数组的原因。)因此,如果您采用这种方法,您是否只是使用一个函数在 JSON 格式和状态形状格式之间来回转换它?这似乎很笨拙。(尽管如果您提倡这种方法,那么您的部分推理是否比上面的对象数组缩减器更笨重?)

3)我知道 Dan​​ Abramov 设计的 redux 在理论上是与状态数据结构无关的(正如“按照惯例,顶级状态是一个对象或其他一些键值集合,如 Map,但从技术上讲,它可以是任何类型,”强调我的)。但是鉴于上述情况,是否只是“建议”将其保留为由 ID 键入的对象,或者我是否会遇到其他无法预料的痛点,方法是使用一组对象,使我应该中止它计划并尝试坚持使用 ID 键入的对象?

4

3 回答 3

48

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

Q2:您可以normalizr用来帮助您从 API 到商店的转换。从 normalizr V3.1.0 开始,您可以使用 denormalize 以另一种方式。也就是说,与数据生产者相比,应用程序通常是更多的消费者,因此通常更频繁地转换为商店。

Q3:使用阵列遇到的问题与其说是存储约定和/或不兼容问题,不如说是性能问题。

于 2016-07-18T20:27:57.887 回答
12

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

这是关键的想法。

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

2) 与 API 的连接不应该影响存储和减速器的架构,这就是为什么你有行动来保持关注点的分离。只需将您的转换逻辑输入和输出 API 中的可重用模块,将该模块导入使用 API 的操作中,就可以了。

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

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

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

还:

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

于 2016-07-18T20:18:22.410 回答
8

1)reducer 的简单性是采用 object-keyed-by-id 方法的动机吗?这种状态形状还有其他优点吗?

您希望将实体保留在以 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) 似乎 object-keyed-by-id 方法使得处理 API 的标准 JSON 输入/输出变得更加困难。(这就是我首先使用对象数组的原因。)因此,如果您采用这种方法,您是否只是使用一个函数在 JSON 格式和状态形状格式之间来回转换它?这似乎很笨拙。(尽管如果您提倡这种方法,那么您的部分推理是否比上面的对象数组缩减器更笨重?)

可以使用例如normalizr对 JSON 响应进行规范化。

3)我知道 Dan​​ Abramov 将 redux 设计为理论上与状态数据结构无关(如“按照惯例,顶级状态是一个对象或其他一些键值集合,如 Map,但从技术上讲它可以是任何类型,”强调我的)。但是鉴于上述情况,是否只是“建议”将其保留为由 ID 键入的对象,或者我是否会遇到其他无法预料的痛点,方法是使用一组对象,使我应该中止它计划并尝试坚持使用 ID 键入的对象?

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

于 2016-07-18T22:16:01.887 回答