管理正規化資料
如 正規化狀態形狀 所述,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
}
}
}
這需要在簡約器方面最少的工作,但確實需要動作建立器在發送動作之前做相當多的工作,將資料整理成正確的形狀。它也不處理嘗試刪除項目。
切片簡約器組成
如果我們有一個巢狀切片簡約器樹狀結構,每個切片簡約器都需要知道如何適當地回應此動作。我們需要在動作中包含所有相關資料。我們需要使用留言 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
})
範例有點長,因為它顯示了所有不同的切片簡約器和案例簡約器如何組合在一起。請注意這裡涉及的委派。postsById
切片簡約器將此案例的工作委派給 addComment
,後者將新的留言 ID 插入正確的文章項目中。同時,commentsById
和 allComments
切片簡約器都有自己的案例簡約器,適當地更新留言查詢表和所有留言 ID 清單。
其他方法
基於任務的更新
由於簡化器只是函數,因此有無限多種方法可以分割這個邏輯。雖然使用區塊簡化器是最常見的方式,但也可以以更以任務為導向的結構來組織行為。由於這通常會涉及更多巢狀更新,因此您可能需要使用不可變的更新實用程式庫,例如 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 提供了一組非常有用的抽象,用於定義資料類型之間的關係、建立狀態中的「表格」、擷取和反正規化關聯資料,以及對關聯資料套用不可變更新。