Skip to main content

管理标准化数据

¥Managing Normalized Data

正如 规范化状态形状 中提到的,Normalizr 库经常用于将嵌套响应数据转换为适合集成到存储中的标准化形状。但是,这并没有解决对规范化数据执行进一步更新的问题,因为它正在应用的其他地方使用。你可以根据自己的喜好使用多种不同的方法。我们将使用处理帖子评论突变的示例。

¥As mentioned in Normalizing State Shape, the Normalizr library is frequently used to transform nested response data into a normalized shape suitable for integration into the store. However, that doesn't address the issue of executing further updates to that normalized data as it's being used elsewhere in the application. There are a variety of different approaches that you can use, based on your own preference. We'll use the example of handling mutations for Comments on a Post.

标准方法

¥Standard Approaches

简单合并

¥Simple Merging

一种方法是将操作的内容合并到现有状态中。在这种情况下,我们可以使用深度递归合并,而不仅仅是浅表复制,以允许使用部分项目的操作来更新存储的项目。Lodash merge 函数可以为我们处理这个问题:

¥One approach is to merge the contents of the action into the existing state. In this case, we can use deep recursive merge, not just a shallow copy, to allow for actions with partial items to update stored items. The Lodash merge function can handle this for us:

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 创建者在分派 action 之前做大量的工作来将数据组织成正确的形状。它也不处理尝试删除项目。

¥This requires the least amount of work on the reducer side, but does require that the action creator potentially do a fair amount of work to organize the data into the correct shape before the action is dispatched. It also doesn't handle trying to delete an item.

切片 Reducer 组成

¥Slice Reducer Composition

如果我们有一个切片 reducer 的嵌套树,则每个切片 reducer 都需要知道如何适当地响应此操作。我们需要在操作中包含所有相关数据。我们需要使用评论的 ID 更新正确的 Post 对象,使用该 ID 作为键创建一个新的 Comment 对象,并将评论的 ID 包含在所有评论 ID 的列表中。以下是各个部分的组合方式:

¥If we have a nested tree of slice reducers, each slice reducer will need to know how to respond to this action appropriately. We will need to include all the relevant data in the action. We need to update the correct Post object with the comment's ID, create a new Comment object using that ID as a key, and include the Comment's ID in the list of all Comment IDs. Here's how the pieces for this might fit together:

// 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
})

该示例有点长,因为它展示了所有不同的 slice reducer 和 case reducer 如何组合在一起。请注意此处涉及的代表团。postsById 切片缩减器将本例的工作委托给 addComment,后者将新评论的 ID 插入到正确的帖子项中。同时,commentsByIdallComments 切片缩减器都有自己的 case 缩减器,它们会相应地更新评论查找表和所有评论 ID 的列表。

¥The example is a bit long, because it's showing how all the different slice reducers and case reducers fit together. Note the delegation involved here. The postsById slice reducer delegates the work for this case to addComment, which inserts the new Comment's ID into the correct Post item. Meanwhile, both the commentsById and allComments slice reducers have their own case reducers, which update the Comments lookup table and list of all Comment IDs appropriately.

其他方法

¥Other Approaches

基于任务的更新

¥Task-Based Updates

由于 reducer 只是函数,因此有无数种方法可以分解此逻辑。虽然使用切片缩减器是最常见的,但也可以以更加面向任务的结构来组织行为。因为这通常会涉及更多嵌套更新,所以你可能需要使用不可变的更新实用程序库(如 dot-prop-immutableobject-path-immutable)来简化更新语句。下面是一个示例:

¥Since reducers are just functions, there's an infinite number of ways to split up this logic. While using slice reducers is the most common, it's also possible to organize behavior in a more task-oriented structure. Because this will often involve more nested updates, you may want to use an immutable update utility library like dot-prop-immutable or object-path-immutable to simplify the update statements. Here's an example of what that might look like:

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 逻辑,这可能是也可能不是需要的。

¥This approach makes it very clear what's happening for the "ADD_COMMENTS" case, but it does require nested updating logic, and some specific knowledge of the state tree shape. Depending on how you want to compose your reducer logic, this may or may not be desired.

Redux-ORM

Redux-ORM 库提供了一个非常有用的抽象层,用于管理 Redux 存储中的规范化数据。它允许你声明模型类并定义它们之间的关系。然后,它可以为你的数据类型生成空的 "tables",充当查找数据的专用选择器工具,并对该数据执行不可变的更新。

¥The Redux-ORM library provides a very useful abstraction layer for managing normalized data in a Redux store. It allows you to declare Model classes and define relations between them. It can then generate the empty "tables" for your data types, act as a specialized selector tool for looking up the data, and perform immutable updates on that data.

Redux-ORM 可以通过多种方式来执行更新。首先,Redux-ORM 文档建议在每个模型子类上定义 reducer 函数,然后将自动生成的组合 reducer 函数包含到你的存储中:

¥There's a couple ways Redux-ORM can be used to perform updates. First, the Redux-ORM docs suggest defining reducer functions on each Model subclass, then including the auto-generated combined reducer function into your 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 库为你维护模型之间的关系。默认情况下,更新是不可变地应用的,从而简化了更新过程。

¥The Redux-ORM library maintains relationships between models for you. Updates are by default applied immutably, simplifying the update process.

另一个变体是使用 Redux-ORM 作为单例缩减器中的抽象层:

¥Another variation on this is to use Redux-ORM as an abstraction layer within a single case reducer:

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
}

通过使用会话接口,你现在可以使用关系访问器直接访问引用的模型:

¥By using the session interface you can now use relationship accessors to directly access referenced models:

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 提供了一组非常有用的抽象,用于定义数据类型之间的关系、在我们的状态中创建 "tables"、检索和反规范化关系数据以及对关系数据应用不可变更新。

¥Overall, Redux-ORM provides a very useful set of abstractions for defining relations between data types, creating the "tables" in our state, retrieving and denormalizing relational data, and applying immutable updates to relational data.