管理规范化数据
正如在 规范化状态形状 中提到的,Normalizr 库经常用于将嵌套的响应数据转换为规范化的形状,适合集成到存储中。但是,这并不能解决在应用程序的其他地方使用时对该规范化数据执行进一步更新的问题。您可以根据自己的喜好使用各种不同的方法。我们将使用处理帖子评论的变异的示例。
标准方法
简单合并
一种方法是将操作的内容合并到现有状态中。在这种情况下,我们可以使用深度递归合并,而不仅仅是浅拷贝,以允许具有部分项目的动作更新存储的项目。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 方面需要最少的工作量,但确实需要操作创建者在分派操作之前可能要花费相当多的工作量来将数据组织成正确的形状。它也不处理尝试删除项目。
切片 reducer 组合
如果我们有一个切片 reducer 的嵌套树,每个切片 reducer 都需要知道如何适当地响应此操作。我们需要在操作中包含所有相关数据。我们需要使用评论的 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 将此 case 的工作委托给 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"
case 的情况,但它确实需要嵌套更新逻辑,以及对状态树形状的一些特定知识。根据您希望如何组合 reducer 逻辑,这可能是或可能不是您想要的。
Redux-ORM
The Redux-ORM 库为管理 Redux 存储中的规范化数据提供了一个非常有用的抽象层。它允许您声明模型类并定义它们之间的关系。然后,它可以为您的数据类型生成空的“表”,充当专门的选择器工具来查找数据,并对该数据执行不可变更新。
Redux-ORM 可以通过几种方式执行更新。首先,Redux-ORM 文档建议在每个模型子类上定义 reducer 函数,然后将自动生成的组合 reducer 函数包含到您的存储中。
// 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 提供了一组非常有用的抽象,用于定义数据类型之间的关系,创建我们状态中的“表”,检索和反规范化关系数据,以及对关系数据应用不可变更新。