本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
管理规范化数据
如规范化状态结构所述,Normalizr 库常被用于将嵌套的响应数据转换为适合集成到 store 中的规范化形态。但这并未解决当规范化数据在应用其他地方使用时如何执行进一步更新的问题。根据个人偏好,您可以采用多种不同方法。我们将以处理帖子(Post)评论(Comment)的变更为例进行说明。
标准方法
简单合并
一种方法是将 action 的内容合并到现有状态中。此时我们可以使用深度递归合并(而非浅拷贝),从而允许包含部分条目的 action 更新存储的条目。Lodash 的 merge 函数可帮助我们实现:
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
}
}
}
这种方式在 reducer 端工作量最小,但需要 action creator 在派发 action 前可能需进行大量工作来组织数据至正确形态。此外,它也无法处理删除条目的情况。
切片 Reducer 组合
如果存在嵌套的切片 reducer 树,每个切片 reducer 都需要知道如何正确响应此 action。我们需要在 action 中包含所有相关数据:需更新正确的帖子对象(添加评论 ID),创建以该 ID 为键的新评论对象,并将评论 ID 包含在所有评论 ID 列表中。以下是各部分的配合方式:
// 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
})
该示例稍长,因为它展示了不同切片 reducer 和 case reducer 如何配合工作。请注意此处的委托机制:postsById 切片 reducer 将工作委托给 addComment(后者将新评论 ID 插入到正确的帖子条目中),而 commentsById 和 allComments 切片 reducer 则分别使用自己的 case reducer 来更新评论查找表和所有评论 ID 列表。
其他方案
基于任务的更新
由于 reducer 本质上是函数,拆分逻辑的方式无限多样。虽然使用切片 reducer 最常见,但也可以按更面向任务的结构组织行为。由于常涉及嵌套更新,您可能需要使用不可变更新工具库(如 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" 场景的处理逻辑,但需要嵌套更新逻辑且需了解状态树的具体形态。根据 reducer 逻辑的组合需求,此方法可能适用也可能不适用。
Redux-ORM
Redux-ORM 库为管理 Redux store 中的规范化数据提供了极有用的抽象层。它允许声明模型(Model)类并定义模型间关系,随后可为数据类型生成空"表",作为专用选择器工具查询数据,并执行不可变的数据更新。
使用 Redux-ORM 执行更新有几种方式。首先,Redux-ORM 文档建议在每个模型子类上定义 reducer 函数,然后将自动生成的组合 reducer 函数包含到 store 中:
// 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 库会维护模型间关系,且默认以不可变方式应用更新,从而简化更新过程。
另一种变体是在单个 case reducer 中将 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
}
通过使用 session 接口,您现在可以直接通过关系访问器引用关联模型:
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 提供了一套极为实用的抽象工具,用于定义数据类型间的关系、在状态中创建"表"结构、检索与反规范化关系数据,以及对关系数据执行不可变更新。