Normalizing State Shape: Why and how to store data items for lookup based on ID">Normalizing State Shape: Why and how to store data items for lookup based on ID">
跳至主要内容

规范化状态形状

许多应用程序处理本质上嵌套或关系型的数据。例如,博客编辑器可以拥有许多帖子,每个帖子可以拥有许多评论,帖子和评论都由用户撰写。此类应用程序的数据可能如下所示

const blogPosts = [
{
id: 'post1',
author: { username: 'user1', name: 'User 1' },
body: '......',
comments: [
{
id: 'comment1',
author: { username: 'user2', name: 'User 2' },
comment: '.....'
},
{
id: 'comment2',
author: { username: 'user3', name: 'User 3' },
comment: '.....'
}
]
},
{
id: 'post2',
author: { username: 'user2', name: 'User 2' },
body: '......',
comments: [
{
id: 'comment3',
author: { username: 'user3', name: 'User 3' },
comment: '.....'
},
{
id: 'comment4',
author: { username: 'user1', name: 'User 1' },
comment: '.....'
},
{
id: 'comment5',
author: { username: 'user3', name: 'User 3' },
comment: '.....'
}
]
}
// and repeat many times
]

请注意,数据的结构有点复杂,并且一些数据重复出现。这会导致几个问题

  • 当数据在多个地方重复时,确保其正确更新变得更加困难。
  • 嵌套数据意味着相应的 reducer 逻辑必须更加嵌套,因此更加复杂。特别是,尝试更新一个深度嵌套的字段可能会很快变得非常难看。
  • 由于不可变数据更新需要复制和更新状态树中的所有祖先,并且新的对象引用会导致连接的 UI 组件重新渲染,因此对深度嵌套数据对象的更新可能会强制完全无关的 UI 组件重新渲染,即使它们显示的数据实际上没有改变。

因此,在 Redux 商店中管理关系或嵌套数据的推荐方法是将商店的一部分视为数据库,并将数据保存在规范化形式中。

设计规范化状态

规范化数据的基本概念是

  • 每种类型的数据在状态中都有自己的“表”。
  • 每个“数据表”都应该将各个项目存储在一个对象中,项目的 ID 作为键,项目本身作为值。
  • 对单个项目的任何引用都应通过存储项目的 ID 来完成。
  • 应使用 ID 数组来指示排序。

上面博客示例的规范化状态结构示例可能如下所示

{
posts : {
byId : {
"post1" : {
id : "post1",
author : "user1",
body : "......",
comments : ["comment1", "comment2"]
},
"post2" : {
id : "post2",
author : "user2",
body : "......",
comments : ["comment3", "comment4", "comment5"]
}
},
allIds : ["post1", "post2"]
},
comments : {
byId : {
"comment1" : {
id : "comment1",
author : "user2",
comment : ".....",
},
"comment2" : {
id : "comment2",
author : "user3",
comment : ".....",
},
"comment3" : {
id : "comment3",
author : "user3",
comment : ".....",
},
"comment4" : {
id : "comment4",
author : "user1",
comment : ".....",
},
"comment5" : {
id : "comment5",
author : "user3",
comment : ".....",
},
},
allIds : ["comment1", "comment2", "comment3", "comment4", "comment5"]
},
users : {
byId : {
"user1" : {
username : "user1",
name : "User 1",
},
"user2" : {
username : "user2",
name : "User 2",
},
"user3" : {
username : "user3",
name : "User 3",
}
},
allIds : ["user1", "user2", "user3"]
}
}

总体而言,这种状态结构更加扁平。与原始嵌套格式相比,这在几个方面有所改进

  • 由于每个项目只在一个地方定义,因此如果该项目更新,我们不必尝试在多个地方进行更改。
  • reducer 逻辑不必处理深层嵌套,因此它可能更简单。
  • 检索或更新给定项目的逻辑现在相当简单且一致。给定项目的类型和 ID,我们可以通过几个简单的步骤直接查找它,而无需深入其他对象来查找它。
  • 由于每种数据类型都是分开的,因此像更改评论文本这样的更新只需要“comments > byId > comment”树部分的新副本。这通常意味着需要更新数据的 UI 部分更少,因为它们的数据已更改。相比之下,在原始嵌套形状中更新评论将需要更新评论对象、父帖子对象、所有帖子对象的数组,并且可能导致 UI 中所有帖子组件和评论组件重新渲染自身。

请注意,规范化的状态结构通常意味着更多组件连接在一起,每个组件负责查找自己的数据,而不是少数连接的组件查找大量数据并将所有数据向下传递。事实证明,让连接的父组件简单地将项目 ID 传递给连接的子组件是优化 React Redux 应用程序中 UI 性能的一个好模式,因此保持状态规范化对于提高性能起着关键作用。

在状态中组织规范化数据

一个典型的应用程序可能会混合使用关系数据和非关系数据。虽然没有一个单一的规则来准确地说明这些不同类型的数据应该如何组织,但一种常见的模式是将关系“表”放在一个公共父键下,例如“entities”。使用这种方法的状态结构可能看起来像

{
simpleDomainData1: {....},
simpleDomainData2: {....},
entities : {
entityType1 : {....},
entityType2 : {....}
},
ui : {
uiSection1 : {....},
uiSection2 : {....}
}
}

这可以以多种方式扩展。例如,一个大量编辑实体的应用程序可能希望在状态中保留两组“表”,一组用于“当前”项目值,另一组用于“正在进行”项目值。当编辑一个项目时,它的值可以复制到“正在进行”部分,并且任何更新它的操作都将应用于“正在进行”副本,允许编辑表单由该数据集控制,而 UI 的另一部分仍然引用原始版本。“重置”编辑表单只需从“正在进行”部分删除项目并将原始数据从“当前”重新复制到“正在进行”即可,而“应用”编辑则涉及将值从“正在进行”部分复制到“当前”部分。

关系和表

因为我们将 Redux 存储的一部分视为“数据库”,所以许多数据库设计原则也适用于此。例如,如果我们有一个多对多关系,我们可以使用一个中间表来建模它,该表存储相应项目的 ID(通常称为“连接表”或“关联表”)。为了保持一致性,我们可能还想使用与实际项目表相同的 byIdallIds 方法,如下所示

{
entities: {
authors : { byId : {}, allIds : [] },
books : { byId : {}, allIds : [] },
authorBook : {
byId : {
1 : {
id : 1,
authorId : 5,
bookId : 22
},
2 : {
id : 2,
authorId : 5,
bookId : 15,
},
3 : {
id : 3,
authorId : 42,
bookId : 12
}
},
allIds : [1, 2, 3]

}
}
}

例如“查找该作者的所有书籍”这样的操作,可以通过在联接表上循环一次轻松完成。考虑到客户端应用程序中典型的数据量和 JavaScript 引擎的速度,这种操作对于大多数用例来说,性能可能足够快。

规范化嵌套数据

由于 API 经常以嵌套形式发送数据,因此在将数据包含在状态树中之前,需要将其转换为规范化的形状。通常使用 Normalizr 库来完成此任务。您可以定义模式类型和关系,将模式和响应数据提供给 Normalizr,它将输出响应的规范化转换。然后,该输出可以包含在操作中并用于更新存储。有关其用法的更多详细信息,请参阅 Normalizr 文档。