Redux 管理范式化数据
如范式化数据章节所提及的,我们经常使用 Normaizr 库将嵌套式数据转化为适合集成到 store 中的范式化数据。
但这并不针对范式化的数据进一步更新后在应用的其他地方使用的问题。根据喜好有很多种可供使用。下面展示像的示例。
标准
试试标准当中常用的简单合并与切片组合方式的延展使用技巧。
一种是将 action 的合并到现有的 state。种情况下,我们需要对数据的深拷贝(非浅拷贝)。Lodash 的 merge 可以帮我们处理这个:
import merge from 'lodash/object/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 dispatch 之前做大量的工作来将数据转化成正确的形态。在数据项时这种方式也是不适合的。
如果我们有由切片 reducer 组成的嵌套数据,每个切片 reducer 都需要知道如何响应这个 action。因为我们需要让 action 囊括所有相关的数据。譬如更新相应的 Post 对象需要 comment 的 id,然后使用 id 作为 key 创建新的 comment 对象,并且让这个 comment 的 id 在所有的 comment id 列表中。下面是如何组合这样数据的例子:
注:结合上章节中范式化之后的 state 阅读
// actions.js function addComment(postId, commentText) { // 为这个 comment 独一无二的 ID 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 // 查找出相应的,简化其余 const post = state[postId] return { ...state, // 用新的 comments 数据更新 Post 对象 [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) { // 省略,这个例子中不需要它 } const postsReducer = combineReducers({ byId: postsById, allIds: allPosts }) // reducers/comments.js function addCommentEntry(state, action) { const { payload } = action const { commentId, commentText } = payload // 创建新的 Comment 对象 const comment = { id: commentId, text: commentText } // 在中插入新的 Comment 对象 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 // 把新 Comment 的 ID 在 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 是如何配合在一起使用的。注意这里对 “委托” 的理解。postById reducer 切片将工作委拖给 addComment,addComment 将新的 id 插入到相应的数据项中。同时 commentsById 和 allComments 的 reducer 切片都有自己的 case reducer,他们更新查找表和所有 id 列表的表。
其他
其他几种常用,供你参考学习。
reducer 仅仅是个,因此有无数种来拆分这个逻辑。使用切片 reducer 是最常见,但也可以在更面向任务的结构中组织行为。由于通常会涉及到更多嵌套的更新,因此常常会使用 、 等库实现不可变更新。
import posts from "./postsReducer"; import comments from "./commentsReducer"; import dotProp from "dot-prop-immutable"; import {combineReducers} from "r"; 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_COMMENT 这个 case 要干哪些事更加清楚,但需要更新嵌套逻辑和对特定状态树的了解。最后这取决于你如何组织 reducer 逻辑,或许你根本不需要这样做。
库提供了非常有用的抽象层,用于管理 R store 中存储的范式化数据。它允许你声明 Model 类并且定义他们之关系。然后它可以为你的数据类型新“表”,充当用于查找数据的特殊选择器工具,并且对数据执行不可变更新。
有几种可以用 R-ORM 执行数据更新。首选,R-ORM 文档建议在每个 Model 子类上定义 reducer ,然后将的组合 reducer 放到 store 中:
// models.js import { Model, many, Schema } from 'r-orm' export class Post extends Model { static get fields() { return { // 定义多边关系 - Post 可以有多个 Comments, // 字段名是 “comments” comments: many('Comment') } } static reducer(state, action, Post) { switch (action.type) { case 'CREATE_POST': { // 排队创建 Post 实例 Post.create(action.payload) break } case 'ADD_COMMENT': { const { payload } = action const { postId, commentId } = payload // 排队 Comment ID 和 Post 实例的联系 Post.withId(postId).comments.add(commentId) break } } // R-ORM 将在返回后应用排队的更新 } } Post.modelName = 'Post' export class Comment extends Model { static get fields() { return {} } static reducer(state, action, Comment) { switch (action.type) { case 'ADD_COMMENT': { const { payload } = action const { commentId, commentText } = payload // 排队创建 Comment 实例 Comment.create({ id: commentId, text: commentText }) break } } // R-ORM 将在返回后应用排队的更新 } } Comment.modelName = 'Comment' // 创建 Schema 实例,然后和 Post、Comment 数据模型挂钩起来 export const schema = new Schema() schema.register(Post, Comment) // main.js import { createStore, combineReducers } from 'r' import { schema } from './models' const rootReducer = combineReducers({ // 插入 R-ORM 的 reducer,这将 // 初始化数据模型 “表”,并且和我们在 // 每个 Model 子类中定义的 reducer 逻辑挂钩起来 entities: schema.reducer() }) // dispatch action 以创建 Post 实例 store.dispatch({ type: 'CREATE_POST', payload: { id: 1, name: 'Test Post Please Ignore' } }) // dispath action 以创建 Comment 实例作为上个 Post 的子元素 store.dispatch({ type: 'ADD_COMMENT', payload: { postId: 1, commentId: 123, commentText: 'This is a comment' } })
R-ORM 库维护要应用的内部更新队列。这些更新是不可变更新,这个库简化了这个更新过程。
使用 R-ORM 的另变化是用单一的 case reducer 作为抽象层。
import { schema } from './models' // 假设这个 case reducer 正在我们的 “entities” 切片 reducer 使用, // 并且我们在 R-ORM 的 Model 子类上没有定义 reducer function addComment(entitiesState, action) { const session = schema.from(entitiesState) const { Post, Comment } = session const { payload } = action const { postId, commentId, commentText } = payload const post = Post.withId(postId) post.comments.add(commentId) Comment.create({ id: commentId, text: commentText }) return session.reduce() }
总之,R-ORM 提供了一组非常有用的抽象,用于定义数据类型之关系,在我们的 state 中创建了 “表”,检索和反规划关系数据,以及将不可变更新应用于关系数据。