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

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

状態構造の正規化

多くのアプリケーションはネストされたデータやリレーショナルな性質のデータを扱います。例えば、ブログエディターは複数の投稿(Post)を持ち、各投稿は複数のコメント(Comment)を持ち、投稿とコメントの両方がユーザー(User)によって作成されます。このようなアプリケーションのデータは次のようになるでしょう:

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: '.....'
}
]
}
// and repeat many times
]

データ構造がやや複雑で、一部のデータが重複していることに注意してください。これは次の理由から問題となります:

  • データが複数箇所で重複している場合、適切に更新することが難しくなる

  • ネストされたデータは対応するリデューサーのロジックをより深くネストさせるため、複雑さが増します。特に深くネストされたフィールドの更新は急速に煩雑になります

  • イミュータブルなデータ更新では状態ツリー内のすべての祖先をコピーして更新する必要があり、新しいオブジェクト参照は接続されたUIコンポーネントの再レンダリングを引き起こします。そのため、深くネストされたデータオブジェクトの更新は、表示しているデータが実際に変更されていない場合でも、まったく関係のないUIコンポーネントの再レンダリングを強制する可能性があります

このため、Reduxストアでリレーショナルデータやネストされたデータを管理する推奨アプローチは、ストアの一部をデータベースのように扱い、データを_正規化_された形式で保持することです

正規化された状態の設計

データを正規化する基本的な概念は以下の通りです:

  • 各データタイプは状態内で独自の「テーブル」を持つ

  • 各「データテーブル」は個々のアイテムをオブジェクトとして保存し、アイテムのIDをキー、アイテム自体を値とする

  • 個々のアイテムへの参照はアイテムのIDを保存することで行う

  • 順序を示すためにはIDの配列を使用する

前述のブログ例に対する正規化された状態構造の例は次のようになります:

{
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", "comment4", "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箇所でのみ定義されるため、更新時に複数箇所を変更する必要がない

  • リデューサーのロジックが深いネストを扱う必要がないため、大幅に簡素化される

  • 特定のアイテムを取得または更新するロジックが非常にシンプルで一貫性を持つ。アイテムのタイプとIDが与えられれば、他のオブジェクトを掘り下げる必要なく、数ステップで直接検索できる

  • 各データタイプが分離されているため、コメントのテキスト変更のような更新では「comments > byId > comment」ツリー部分の新しいコピーのみが必要です。これは一般にデータ変更によるUI更新箇所が少なくなることを意味します。対照的に、元のネスト形式でコメントを更新する場合、コメントオブジェクト、親投稿オブジェクト、すべての投稿オブジェクトの配列を更新する必要があり、UI内の_すべて_のPostコンポーネントとCommentコンポーネントが再レンダリングされる可能性があります

正規化された状態構造は一般に、より多くのコンポーネントが接続され、各コンポーネントが自身のデータを検索する責任を持つことを意味することに注意してください。これは、少数の接続されたコンポーネントが大量のデータを検索してそのデータを下方に渡す方法とは対照的です。結局のところ、接続された親コンポーネントが単にアイテムIDを接続された子コンポーネントに渡すことは、React ReduxアプリケーションでUIパフォーマンスを最適化する優れたパターンであり、状態を正規化することはパフォーマンス向上に重要な役割を果たします

状態内での正規化データの整理

典型的なアプリケーションでは、リレーショナルデータと非リレーショナルデータが混在する可能性があります。これらの異なるタイプのデータを整理する厳密なルールはありませんが、一般的なパターンの1つは、リレーショナルな「テーブル」を「entities」などの共通の親キーの下に配置することです。このアプローチを使用した状態構造は次のようになります:

{
simpleDomainData1: {....},
simpleDomainData2: {....},
entities: {
entityType1 : {....},
entityType2 : {....}
},
ui: {
uiSection1 : {....},
uiSection2 : {....}
}
}

これはさまざまな方法で拡張可能です。例えば、エンティティの編集が多いアプリケーションでは、状態内に2つの「テーブル」セットを保持する方法があります。1つは「現在」のアイテム値用、もう1つは「作業中」のアイテム値用です。アイテムが編集されると、その値が「作業中」セクションにコピーされ、更新アクションは「作業中」コピーに適用されます。これにより編集フォームはこのデータセットで制御されつつ、他のUI部分は元のバージョンを参照し続けられます。編集フォームの「リセット」は、「作業中」セクションからアイテムを削除し「現在」から元データを再コピーするだけで済み、「編集の適用」は「作業中」セクションの値を「現在」セクションにコピーすることを意味します。

リレーションシップとテーブル

Reduxストアの一部を「データベース」として扱うため、データベース設計の原則の多くがここでも適用されます。例えば多対多リレーションシップがある場合、対応するアイテムのIDを格納する中間テーブル(「結合テーブル」や「関連テーブル」とも呼ばれる)を使用してモデル化できます。一貫性を保つため、実際のアイテムテーブルに使用したのと同じbyIdallIdsアプローチを採用すると次のようになります:

{
entities: {
authors: {
byId: {},
allIds: []
},
books: {
byId: {},
allIds: []
},
authorBook: {
byId: {
1: {
id: 1,
authorId: 5,
bookId: 22
},
2: {
id: 2,
authorId: 5,
bookId: 15
},
3: {
id: 3,
authorId: 42,
bookId: 12
}
},
allIds: [1, 2, 3]
}
}
}

「この著者による全ての書籍を検索」といった操作は、結合テーブルを1回ループするだけで簡単に実現できます。クライアントアプリケーションで扱う典型的なデータ量とJavaScriptエンジンの速度を考慮すると、この種の操作はほとんどのユースケースで十分なパフォーマンスを発揮します。

ネストされたデータの正規化

APIは頻繁にネスト形式でデータを返すため、状態ツリーに組み込む前に正規化された形式へ変換する必要があります。通常はNormalizrライブラリがこのタスクに使用されます。スキーマタイプとリレーションを定義し、スキーマとレスポンスデータをNormalizrに渡すと、正規化された変換結果が出力されます。この出力をアクションに含めてストアの更新に使用できます。詳細な使用方法についてはNormalizrのドキュメントを参照してください。