正規化されたデータの管理
状態形状の正規化で述べたように、Normalizrライブラリは、ネストされたレスポンスデータをストアへの統合に適した正規化された形状に変換するためによく使用されます。ただし、それは、アプリケーションの他の場所で使用されている正規化されたデータに対するさらなる更新を実行するという問題に対処していません。個人の好みに基づいて、使用できるさまざまなアプローチがあります。ここでは、投稿のコメントのミューテーションを処理する例を使用します。
標準的なアプローチ
単純なマージ
1つのアプローチは、アクションの内容を既存の状態にマージすることです。この場合、部分的な項目を持つアクションが保存された項目を更新できるように、浅いコピーだけでなく、深い再帰的なマージを使用できます。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で正しいPostオブジェクトを更新し、そのIDをキーとして使用して新しいCommentオブジェクトを作成し、すべてのコメント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
に委任します。addComment
は、新しいコメントのIDを正しいPost項目に挿入します。一方、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ストアで正規化されたデータを管理するための非常に便利な抽象化レイヤーを提供します。Modelクラスを宣言し、それらの間の関係を定義することができます。その後、データ型の空の「テーブル」を生成し、データを検索するための特殊なセレクターツールとして機能し、そのデータに対してイミュータブルな更新を実行できます。
Redux-ORMを使用して更新を実行するには、いくつかの方法があります。まず、Redux-ORMのドキュメントでは、各Modelサブクラスでリデューサー関数を定義し、自動生成された結合リデューサー関数をストアに含めることを推奨しています。
// 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は、データ型間の関係を定義し、状態に「テーブル」を作成し、リレーショナルデータを取得および非正規化し、リレーショナルデータにイミュータブルな更新を適用するための非常に便利な抽象化セットを提供します。