# Redux - Normalization

# 非正規データの弊害

  • ネストされたデータをそのまま扱うことには下記の弊害がある
    • 同一データが複数の場所に散在するため、一貫性のある更新が困難
    • ネストしたデータを更新する reducer は見づらい
    • ネストしたデータを更新すると祖先のデータも更新され、結果として大量の再描写が発生する
  • このことから、正規化してデータを持つことが推奨される
// 非正規データの例
const blogPosts = [
  {
    id: 'post1',
    author: { username: 'user1', name: 'User 1' },
    body: '......',
    comments: [
      {
        id: 'comment1',
        author: { username: 'user2', name: 'User 2' },
        comment: '.....',
      },
      {
        id: 'comment2',
        author: { username: 'user3', name: 'User 3' },
        comment: '.....',
      },
    ],
  },
  {
    id: 'post2',
    author: { username: 'user2', name: 'User 2' },
    body: '......',
    comments: [
      {
        id: 'comment3',
        author: { username: 'user3', name: 'User 3' },
        comment: '.....',
      },
      {
        id: 'comment4',
        author: { username: 'user1', name: 'User 1' },
        comment: '.....',
      },
      {
        id: 'comment5',
        author: { username: 'user3', name: 'User 3' },
        comment: '.....',
      },
    ],
  },
];

# 正規化の基本コンセプト

  • データの種類ごとに「テーブル」を state に作成する
  • テーブルは以下の 2 つのオブジェクトを持つ
    • 「キーが ID、値がデータ」のオブジェクト(下記のbyId)
      • ここで実際のデータを管理する
      • 個々のアイテムを参照するときは ID を使って行う
    • 全ての ID を持つ配列(下記のallIds)
  • 必要に応じてNormalizrなどのライブラリを使うと良い
// 正規化済みのデータ
const normalizedState = {
  posts: {
    byId: {
      post1: {
        id: 'post1',
        author: 'user1',
        body: '......',
        comments: ['comment1', 'comment2'],
      },
      post2: {
        id: 'post2',
        author: 'user2',
        body: '......',
        comments: ['comment3', 'comment4', 'comment5'],
      },
    },
    allIds: ['post1', 'post2'],
  },
  comments: {
    byId: {
      comment1: {
        id: 'comment1',
        author: 'user2',
        comment: '.....',
      },
      comment2: {
        id: 'comment2',
        author: 'user3',
        comment: '.....',
      },
      comment3: {
        id: 'comment3',
        author: 'user3',
        comment: '.....',
      },
      comment4: {
        id: 'comment4',
        author: 'user1',
        comment: '.....',
      },
      comment5: {
        id: 'comment5',
        author: 'user3',
        comment: '.....',
      },
    },
    allIds: ['comment1', 'comment2', 'comment3', 'commment4', 'comment5'],
  },
  users: {
    byId: {
      user1: {
        username: 'user1',
        name: 'User 1',
      },
      user2: {
        username: 'user2',
        name: 'User 2',
      },
      user3: {
        username: 'user3',
        name: 'User 3',
      },
    },
    allIds: ['user1', 'user2', 'user3'],
  },
};

# 正規化のメリット

  • データの更新は 1 箇所だけ行えば良い
  • reducer において深くネストしたデータを更新しなくていい
  • データの取得や更新がシンプルかつ一貫性を持ったものになる
    • データの種類と ID を指定するだけで、検索をかけることなく直接ルックアップできる
  • 更新時の UI への影響範囲が最小限になる

正規化を行うと「限られたコンポーネントが store に接続して、ネストした大量のデータを取得して子孫に渡していく」スタイルから「多くのコンポーネントが個々に store に接続して最小限のデータを取得する」ようになる。

親も子孫も store に接続したうえで、親から子へ渡す props は id だけ、というパターンにしておくのが、もっとも UI パフォーマンスがよい。

# state の構成例

  • 正規化したデータは共通のキー(entitiesなど)の下に集約するのがおすすめ
  • 現在の値と編集中の値を分ける方法の例
    • 編集 --- "entities"から必要なデータを"work-in-progress"な場所にコピーして編集する
    • 適用 --- "work-in-progress"な場所から"entities"にデータをコピーする
    • 編集のリセット --- "work-in-progress"な場所をクリアし、"entities"から必要なデータを"work-in-progress"な場所に再びコピーする
{
    simpleDomainData1: {....},
    simpleDomainData2: {....},
    entities : {
        entityType1 : {....},
        entityType2 : {....}
    },
    ui : {
        uiSection1 : {....},
        uiSection2 : {....}
    }
}

# 正規化したデータの更新方法

  • 例えば前述の Comments を更新する場合の方法を検討してみよう
  • この場合、下記の 4 つの state の更新が必要になる
    • state.entities.posts['該当するpostId'].commentsにコメント ID を追加
    • state.entities.comments.byIdにコメントを追加
    • state.entities.comments.allIdsにコメント ID を追加

# スタンダードな方法

# action 側で正規化する方法

  • action や middleware で正規化してアクションを発出する
    • 例えば{action:{entitiies:{ここに正規化したデータを必ず配置する}}}などの形式で
  • reducer を action をまたいで共通化できるメリットがある
    • どのアクションでもaction.entities.***を UPSERT などすればよいため
  • API レスポンスに変更があったときは action creator 側の修正が必要

# reducer 側で正規化する方法

  • reducer 側で非正規なデータから必要なデータを抜き出して適切に処理する
  • reducer は action ごとに個別となる
  • API レスポンスに変更があったときは reducer 側の修正が必要

# サンプル

real-world example

このサンプルではミドルウェア層において、正規化の処理や、スネークケースからキャメルケースへの変換を一括して行っている。

# その他の方法 (Task-Based Updates)

  • タスク(コメントの追加など)で必要となる処理を一括で記載した reducer を作成する方法
  • この reducer はトップレベルに配置する必要がある
  • immerを内包したredux-toolkitを使えばコードはかなりすっきりする
  • タスクで必要となる処理が見渡せる一方で、state 全体の知識が必要になり関心の分離が出来ないのが難点
import { createReducer } from '@reduxjs/toolkit';
const addCommentFeatureReducer = createReducer(
  {},
  {
    ADD_COMMENT: (state, action) => {
      const { payload } = action;
      const { postId, commentId, commentText } = payload;

      state.posts.byId[postId].comments.concat(commentId);
      state.comments.byId[commentId] = { id: commentId, text: commentText };
      state.comments.allIds.concat(commentId);
    },
  },
);