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

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

Управление нормализованными данными

Как упоминалось в разделе Нормализация структуры состояния, библиотека Normalizr часто используется для преобразования вложенных данных ответа в нормализованную форму, подходящую для интеграции в хранилище. Однако это не решает проблему выполнения дальнейших обновлений этих нормализованных данных при их использовании в других частях приложения. Существует множество различных подходов, которые вы можете использовать в зависимости от ваших предпочтений. Мы рассмотрим пример обработки изменений комментариев к публикации.

Стандартные подходы

Простое слияние

Один из подходов — объединение содержимого действия с существующим состоянием. В этом случае мы можем использовать глубокое рекурсивное слияние, а не поверхностное копирование, чтобы позволить действиям с частичными элементами обновлять сохранённые элементы. Функция merge из Lodash может справиться с этим:

import merge from 'lodash/merge'

function commentsById(state = {}, action) {
switch (action.type) {
default: {
if (action.entities && action.entities.comments) {
return merge({}, state, action.entities.comments.byId)
}
return state
}
}
}

Это требует минимальных усилий со стороны редюсера, но потенциально требует от создателя действия значительной работы по организации данных в правильную форму перед отправкой действия. Кроме того, этот подход не поддерживает удаление элементов.

Композиция редюсеров срезов

Если у нас есть вложенное дерево редюсеров срезов, каждый из них должен соответствующим образом реагировать на действие. Нам потребуется включить все релевантные данные в действие. Необходимо обновить правильный объект публикации с идентификатором комментария, создать новый объект комментария, используя этот идентификатор как ключ, и включить идентификатор комментария в список всех идентификаторов комментариев. Вот как могут взаимодействовать компоненты:

// actions.js
function addComment(postId, commentText) {
// Generate a unique ID for this comment
const commentId = generateId('comment')

return {
type: 'ADD_COMMENT',
payload: {
postId,
commentId,
commentText
}
}
}

// reducers/posts.js
function addComment(state, action) {
const { payload } = action
const { postId, commentId } = payload

// Look up the correct post, to simplify the rest of the code
const post = state[postId]

return {
...state,
// Update our Post object with a new "comments" array
[postId]: {
...post,
comments: post.comments.concat(commentId)
}
}
}

function postsById(state = {}, action) {
switch (action.type) {
case 'ADD_COMMENT':
return addComment(state, action)
default:
return state
}
}

function allPosts(state = [], action) {
// omitted - no work to be done for this example
}

const postsReducer = combineReducers({
byId: postsById,
allIds: allPosts
})

// reducers/comments.js
function addCommentEntry(state, action) {
const { payload } = action
const { commentId, commentText } = payload

// Create our new Comment object
const comment = { id: commentId, text: commentText }

// Insert the new Comment object into the updated lookup table
return {
...state,
[commentId]: comment
}
}

function commentsById(state = {}, action) {
switch (action.type) {
case 'ADD_COMMENT':
return addCommentEntry(state, action)
default:
return state
}
}

function addCommentId(state, action) {
const { payload } = action
const { commentId } = payload
// Just append the new Comment's ID to the list of all IDs
return state.concat(commentId)
}

function allComments(state = [], action) {
switch (action.type) {
case 'ADD_COMMENT':
return addCommentId(state, action)
default:
return state
}
}

const commentsReducer = combineReducers({
byId: commentsById,
allIds: allComments
})

Пример немного длинный, поскольку демонстрирует взаимодействие всех различных редюсеров срезов и обработчиков. Обратите внимание на делегирование: редюсер среза postsById передаёт работу для этого случая функции addComment, которая вставляет новый идентификатор комментария в соответствующую публикацию. Тем временем редюсеры срезов commentsById и allComments имеют собственные обработчики, которые соответственно обновляют таблицу комментариев и список всех идентификаторов комментариев.

Другие подходы

Задачно-ориентированные обновления

Поскольку редюсеры — это просто функции, существует бесконечное число способов разделить эту логику. Хотя использование редюсеров срезов наиболее распространено, также возможно организовать поведение в более задачно-ориентированную структуру. Поскольку это часто включает вложенные обновления, вы можете использовать библиотеки для неизменяемых обновлений, такие как dot-prop-immutable или object-path-immutable, чтобы упростить выражения обновлений. Вот пример:

import posts from "./postsReducer";
import comments from "./commentsReducer";
import dotProp from "dot-prop-immutable";
import {combineReducers} from "redux";
import reduceReducers from "reduce-reducers";

const combinedReducer = combineReducers({
posts,
comments
});


function addComment(state, action) {
const {payload} = action;
const {postId, commentId, commentText} = payload;

// State here is the entire combined state
const updatedWithPostState = dotProp.set(
state,
`posts.byId.${postId}.comments`,
comments => comments.concat(commentId)
);

const updatedWithCommentsTable = dotProp.set(
updatedWithPostState,
`comments.byId.${commentId}`,
{id : commentId, text : commentText}
);

const updatedWithCommentsList = dotProp.set(
updatedWithCommentsTable,
`comments.allIds`,
allIds => allIds.concat(commentId);
);

return updatedWithCommentsList;
}

const featureReducers = createReducer({}, {
ADD_COMMENT : addComment,
});

const rootReducer = reduceReducers(
combinedReducer,
featureReducers
);

Этот подход делает очевидным происходящее для случая "ADD_COMMENTS", но требует логики вложенных обновлений и знания структуры дерева состояния. В зависимости от желаемой композиции логики редюсеров, это может быть приемлемо или нет.

Redux-ORM

Библиотека Redux-ORM предоставляет полезный уровень абстракции для управления нормализованными данными в хранилище Redux. Она позволяет объявлять классы моделей и определять связи между ними. Затем она может генерировать пустые "таблицы" для типов данных, выступать как специализированный инструмент выборки данных и выполнять неизменяемые обновления.

Есть несколько способов использования Redux-ORM для выполнения обновлений. Во-первых, документация Redux-ORM рекомендует определять функции редюсеров в каждом подклассе модели, затем включать автоматически сгенерированную объединённую функцию редюсера в хранилище:

// models.js
import { Model, fk, attr, ORM } from 'redux-orm'

export class Post extends Model {
static get fields() {
return {
id: attr(),
name: attr()
}
}

static reducer(action, Post, session) {
switch (action.type) {
case 'CREATE_POST': {
Post.create(action.payload)
break
}
}
}
}
Post.modelName = 'Post'

export class Comment extends Model {
static get fields() {
return {
id: attr(),
text: attr(),
// Define a foreign key relation - one Post can have many Comments
postId: fk({
to: 'Post', // must be the same as Post.modelName
as: 'post', // name for accessor (comment.post)
relatedName: 'comments' // name for backward accessor (post.comments)
})
}
}

static reducer(action, Comment, session) {
switch (action.type) {
case 'ADD_COMMENT': {
Comment.create(action.payload)
break
}
}
}
}
Comment.modelName = 'Comment'

// Create an ORM instance and hook up the Post and Comment models
export const orm = new ORM()
orm.register(Post, Comment)

// main.js
import { createStore, combineReducers } from 'redux'
import { createReducer } from 'redux-orm'
import { orm } from './models'

const rootReducer = combineReducers({
// Insert the auto-generated Redux-ORM reducer. This will
// initialize our model "tables", and hook up the reducer
// logic we defined on each Model subclass
entities: createReducer(orm)
})

// Dispatch an action to create a Post instance
store.dispatch({
type: 'CREATE_POST',
payload: {
id: 1,
name: 'Test Post Please Ignore'
}
})

// Dispatch an action to create a Comment instance as a child of that Post
store.dispatch({
type: 'ADD_COMMENT',
payload: {
id: 123,
text: 'This is a comment',
postId: 1
}
})

Redux-ORM автоматически поддерживает связи между моделями. Обновления по умолчанию применяются неизменяемым образом, что упрощает процесс.

Другой вариант — использовать Redux-ORM как уровень абстракции внутри одного обработчика действий:

import { orm } from './models'

// Assume this case reducer is being used in our "entities" slice reducer,
// and we do not have reducers defined on our Redux-ORM Model subclasses
function addComment(entitiesState, action) {
// Start an immutable session
const session = orm.session(entitiesState)

session.Comment.create(action.payload)

// The internal state reference has now changed
return session.state
}

Используя интерфейс сессии, вы теперь можете напрямую обращаться к связанным моделям через методы доступа к отношениям:

const session = orm.session(store.getState().entities)
const comment = session.Comment.first() // Comment instance
const { post } = comment // Post instance
post.comments.filter(c => c.text === 'This is a comment').count() // 1

В целом, Redux-ORM предоставляет крайне полезный набор абстракций для: определения связей между типами данных, создания "таблиц" в нашем состоянии, получения и денормализации реляционных данных, а также применения неизменяемых обновлений к реляционным данным.