状態シェイプの正規化
多くのアプリケーションは、ネストされた、あるいは関係性のあるデータを扱います。たとえば、ブログエディタには多数の投稿があり、各投稿には多数のコメントがあり、投稿とコメントはどちらもユーザーによって書かれています。この種のアプリケーションのデータは、次のようになります。
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
]
データの構造が少し複雑で、一部のデータが繰り返されていることに注意してください。これは、いくつかの理由で懸念事項です。
- データの一部が複数の場所に重複していると、適切に更新されていることを確認するのが難しくなります。
- ネストされたデータは、対応するreducerロジックがよりネストされ、したがってより複雑になる必要があることを意味します。特に、深くネストされたフィールドを更新しようとすると、非常に醜くなる可能性があります。
- イミュータブルなデータ更新では、状態ツリー内のすべての祖先もコピーして更新する必要があるため、新しいオブジェクト参照によって接続された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か所でのみ定義されているため、そのアイテムが更新された場合に複数の場所で変更を加えようとする必要はありません。
- reducerロジックは深いネストレベルを処理する必要がないため、おそらくはるかに単純になります。
- 特定のアイテムを取得または更新するためのロジックは、かなり単純で一貫性があります。アイテムのタイプとそのIDが与えられると、他のオブジェクトを掘り下げて見つけることなく、簡単な手順で直接検索できます.
- 各データ型は分離されているため、コメントのテキストを変更するなどの更新では、ツリーの「comments > byId > comment」部分の新しいコピーのみが必要になります。これは一般に、データが変更されたために更新する必要があるUIの部分が少なくなることを意味します。対照的に、元のネストされたシェイプでコメントを更新すると、コメントオブジェクト、親投稿オブジェクト、すべての投稿オブジェクトの配列を更新する必要があり、UIの*すべて*の投稿コンポーネントとコメントコンポーネントが再レンダリングされる可能性があります。
正規化された状態構造は、一般に、少数の接続されたコンポーネントが大量のデータを検索してすべてのデータを下方に渡すのではなく、より多くのコンポーネントが接続され、各コンポーネントが独自のデータを検索する役割を担うことを意味することに注意してください。 実際には、接続された親コンポーネントがアイテムIDを接続された子に渡すことは、React ReduxアプリケーションでUIのパフォーマンスを最適化するための優れたパターンであることが判明しているため、状態の正規化を維持することは、パフォーマンスの向上に重要な役割を果たします。
状態での正規化されたデータの構成
典型的なアプリケーションは、リレーショナルデータと非リレーショナルデータが混在している可能性があります。 これらの異なるタイプのデータを正確にどのように構成する必要があるかについての単一のルールはありませんが、1つの一般的なパターンは、リレーショナル「テーブル」を「エンティティ」などの共通の親キーの下に置くことです。 このアプローチを使用する状態構造は、次のようになります。
{
simpleDomainData1: {....},
simpleDomainData2: {....},
entities : {
entityType1 : {....},
entityType2 : {....}
},
ui : {
uiSection1 : {....},
uiSection2 : {....}
}
}
これは、さまざまな方法で拡張できます。 たとえば、エンティティの編集を多数行うアプリケーションは、状態に2セットの「テーブル」を保持したい場合があります。1つは「現在」のアイテム値用、もう1つは「作業中」のアイテム値用です。 アイテムが編集されると、その値は「作業中」セクションにコピーされ、それを更新するアクションは「作業中」のコピーに適用されます。これにより、編集フォームをそのデータセットで制御できるようになります。 UIの別の部分は引き続き元のバージョンを参照します。 編集フォームを「リセット」するには、アイテムを「作業中」セクションから削除し、元のデータを「現在」から「作業中」に再コピーするだけで済みます。一方、編集を「適用」するには、値をコピーする必要があります。 「作業中」セクションから「現在」セクションへ。
関係とテーブル
Reduxストアの一部を「データベース」として扱っているため、データベース設計の多くの原則もここに適用されます。 たとえば、多対多の関係がある場合、対応するアイテムのIDを格納する中間テーブル(多くの場合、「結合テーブル」または「関連テーブル」と呼ばれます)を使用してモデル化できます。 一貫性のために、実際のアイテムテーブルに使用したものと同じ `byId` および `allIds` アプローチを使用することをお勧めします。次のようにします。
{
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のドキュメントを参照してください。