メインコンテンツへスキップ
非公式ベータ版翻訳

このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →

正規化データの管理

正規化されたステートの構造で述べたように、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で更新し、その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を適切な投稿アイテムに挿入します。一方、commentsByIdallComments のスライスリデューサはそれぞれ独自のケースリデューサを持ち、コメントのルックアップテーブルと全コメントIDリストを適切に更新します。

その他のアプローチ

タスクベースの更新

リデューサは単なる関数であるため、このロジックを分割する方法は無限にあります。スライスリデューサを使用するのが最も一般的ですが、よりタスク指向の構造で動作を整理することも可能です。ネストされた更新が多くなるため、dot-prop-immutableobject-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は、データ型間のリレーション定義、ステート内の「テーブル」作成、リレーショナルデータの取得と非正規化、リレーショナルデータへの不変更新適用といった非常に有用な抽象化機能セットを提供します。