標準化狀態形狀
許多應用程式處理的資料在性質上是巢狀或關聯的。例如,部落格編輯器可能有多篇文章,每篇文章可能有多則留言,而文章和留言都是由使用者撰寫的。此類應用程式的資料可能如下所示
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"]
}
}
這個狀態結構整體上平坦許多。與原始巢狀格式相比,這在幾個方面都有所改善
- 由於每個項目只在一個地方定義,因此如果更新該項目,我們不必嘗試在多個地方進行變更。
- reducer 邏輯不必處理深度巢狀層級,因此可能會簡單得多。
- 擷取或更新特定項目的邏輯現在相當簡單且一致。有了項目的類型和 ID,我們可以直接在幾個簡單的步驟中查詢它,而無需深入挖掘其他物件來尋找它。
- 由於每種類型的資料都是分開的,因此像變更留言文字這樣的更新只需要「留言 > 依 ID > 留言」樹狀結構部分的新副本。這通常表示需要更新的 UI 部分較少,因為它們的資料已變更。相反地,更新原始巢狀形狀中的留言需要更新留言物件、父層文章物件、所有文章物件的陣列,而且可能會導致 UI 中所有的文章元件和留言元件重新渲染自己。
請注意,正規化的狀態結構通常表示有更多元件連線,且每個元件負責查詢自己的資料,而不是少數幾個連線元件查詢大量資料並向下傳遞所有資料。事實證明,讓連線的父元件僅將項目 ID 傳遞給連線的子元件,是最佳化 React Redux 應用程式 UI 效能的良好範例,因此維持正規化的狀態在提升效能方面扮演著關鍵角色。
在狀態中整理正規化資料
典型的應用程式可能會混合關聯資料和非關聯資料。雖然沒有單一規則可以明確指出這些不同類型的資料應該如何整理,但一個常見的範例是將關聯式「表格」放在共同的父項關鍵字下,例如「實體」。使用此方法的狀態結構可能如下所示
{
simpleDomainData1: {....},
simpleDomainData2: {....},
entities : {
entityType1 : {....},
entityType2 : {....}
},
ui : {
uiSection1 : {....},
uiSection2 : {....}
}
}
這可以用許多方式擴充。例如,大量編輯實體的應用程式可能想要在狀態中保留兩組「表格」,一組用於「目前的」項目值,另一組用於「進行中」的項目值。當編輯項目時,其值可以複製到「進行中」區段,而任何更新它的動作都會套用至「進行中」副本,讓編輯表單由該組資料控制,而 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]
}
}
}
像「查詢此作者的所有書籍」這類的運算,就能利用單一迴圈輕鬆地透過連接表格來達成。考量到客戶端應用程式中資料的典型數量和 JavaScript 引擎的速度,這種運算在大部分的使用案例中,效能應該都足夠快。
正規化巢狀資料
由於 API 經常以巢狀形式傳回資料,因此在納入狀態樹之前,需要將資料轉換成正規化的形式。通常會使用 Normalizr 函式庫來執行這項任務。您可以定義架構類型和關聯,將架構和回應資料提供給 Normalizr,它會輸出回應的正規化轉換。然後可以將該輸出納入動作中,並用來更新儲存。有關其使用方式的更多詳細資訊,請參閱 Normalizr 文件。