Перейти к основному содержимому
Неофициальный Бета-перевод

Эта страница переведена PageTurner AI (бета). Не одобрена официально проектом. Нашли ошибку? Сообщить о проблеме →

Нормализация структуры состояния

Многие приложения работают с вложенными или реляционными данными. Например, редактор блога может содержать множество постов (Posts), каждый пост может иметь множество комментариев (Comments), а и посты, и комментарии создаются пользователями (User). Данные для такого приложения могут выглядеть так:

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
]

Обратите внимание, что структура данных довольно сложная, а часть данных дублируется. Это вызывает несколько проблем:

  • Когда данные дублируются в нескольких местах, сложнее гарантировать их корректное обновление.

  • Вложенные данные требуют более сложной вложенной логики редюсеров. В частности, обновление глубоко вложенного поля может быстро стать очень громоздким.

  • Поскольку неизменяемые обновления данных требуют копирования и обновления всех родительских элементов в дереве состояния, а новые ссылки на объекты приводят к повторному рендерингу подключённых 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"]
}
}

Эта структура состояния значительно более плоская. По сравнению с исходным вложенным форматом, она имеет несколько преимуществ:

  • Поскольку каждый элемент определён только в одном месте, при его обновлении не нужно вносить изменения в нескольких местах.

  • Логике редюсеров не нужно работать с глубокими уровнями вложенности, что делает её проще.

  • Получение или обновление элемента теперь выполняется просто и единообразно. Зная тип элемента и его ID, мы можем напрямую найти его за пару шагов, без поиска по другим объектам.

  • Поскольку типы данных разделены, обновление (например, текста комментария) потребует новых копий только в части дерева "comments > byId > comment". Обычно это означает меньше областей UI, требующих обновления. В отличие от этого, обновление комментария в исходной вложенной структуре потребовало бы обновления объекта комментария, родительского объекта поста, массива всех постов и, вероятно, вызвало бы перерисовку всех компонентов постов и комментариев.

Заметьте, что нормализованная структура состояния обычно подразумевает больше подключённых компонентов, где каждый компонент сам отвечает за получение своих данных, в отличие от модели, где несколько компонентов получают большие объёмы данных и передают их вниз. Как показывает практика, передача ID элементов от родительских компонентов дочерним — хороший шаблон для оптимизации производительности UI в React Redux приложениях, поэтому нормализация состояния играет ключевую роль в повышении производительности.

Организация нормализованных данных в состоянии

Типичное приложение обычно содержит смесь реляционных и нереляционных данных. Хотя нет строгих правил организации таких данных, распространённый подход — размещение реляционных "таблиц" под общим родительским ключом, например "entities". Пример структуры состояния с таким подходом:

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

Этот подход можно расширить различными способами. Например, приложение, которое часто редактирует сущности, может хранить два набора "таблиц" в состоянии: один для "текущих" значений элементов, а другой — для их "черновых" версий. При редактировании элемента его значения копируются в раздел "черновиков", и все обновляющие действия применяются к этой копии. Это позволяет форме редактирования управляться черновыми данными, в то время как другие части интерфейса продолжают использовать оригинальные значения. "Сброс" формы потребует лишь удаления элемента из раздела "черновиков" и повторного копирования исходных данных из "текущих" значений. "Применение" изменений будет заключаться в копировании данных из "черновиков" в "текущие" значения.

Связи и таблицы

Поскольку мы рассматриваем часть хранилища Redux как "базу данных", принципы проектирования БД здесь также применимы. Например, для связи «многие ко многим» можно создать промежуточную таблицу, хранящую ID связанных элементов (известную как "join table" или "ассоциативная таблица"). Для единообразия стоит использовать подход с byId и allIds, как в основных таблицах:

{
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, а она возвращает нормализованный объект. Этот результат можно включить в действие (action) для обновления хранилища. Подробности использования см. в документации Normalizr.