Эта страница переведена 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 предоставляет крайне полезный набор абстракций для: определения связей между типами данных, создания "таблиц" в нашем состоянии, получения и денормализации реляционных данных, а также применения неизменяемых обновлений к реляционным данным.