# 管理范式化數據
如 [范式化數據](./NormalizingStateShape.md) 章節所提及的,我們經常使用 Normaizr 庫將嵌套式數據轉化為適合集成到 store 中的范式化數據。但這并不解決針對范式化的數據進一步更新后在應用的其他地方使用的問題。根據喜好有很多種方法可供使用。下面展示一個像文章添加評論的示例。
## 標準方法
### 簡單合并
一種方法是將 action 的內容合并到現有的 state。在這種情況下,我們需要一個對數據的深拷貝(非淺拷貝)。Lodash 的 `merge` 方法可以幫我們處理這個:
```javascript
import merge from 'lodash/object/merge'
function commentsById(state = {}, action) {
switch (action.type) {
default: {
if (action.entities && action.entities.comments) {
return merge({}, state, action.entities.comments.byId)
}
return state
}
}
}
```
這樣做會讓 reducer 保持最小的工作量,但需要 action creator 在 action dispatch 之前做大量的工作來將數據轉化成正確的形態。在刪除數據項時這種方式也是不適合的。
### reducer 切片組合
如果我們有一個由切片 reducer 組成的嵌套數據,每個切片 reducer 都需要知道如何響應這個 action。因為我們需要讓 action 囊括所有相關的數據。譬如更新相應的 Post 對象需要生成一個 comment 的 id,然后使用 id 作為 key 創建一個新的 comment 對象,并且讓這個 comment 的 id 包括在所有的 comment id 列表中。下面是一個如何組合這樣數據的例子:
> 譯者注:結合上章節中范式化之后的 state 閱讀
```javascript
// actions.js
function addComment(postId, commentText) {
// 為這個 comment 生成一個獨一無二的 ID
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
// 查找出相應的文章,簡化其余代碼
const post = state[postId]
return {
...state,
// 用新的 comments 數據更新 Post 對象
[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) {
// 省略,這個例子中不需要它
}
const postsReducer = combineReducers({
byId: postsById,
allIds: allPosts
})
// reducers/comments.js
function addCommentEntry(state, action) {
const { payload } = action
const { commentId, commentText } = payload
// 創建一個新的 Comment 對象
const comment = { id: commentId, text: commentText }
// 在查詢表中插入新的 Comment 對象
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
// 把新 Comment 的 ID 添加在 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
})
```
這個例子之所有有點長,是因為它展示了不同切片 reducer 和 case reducer 是如何配合在一起使用的。注意這里對 “委托” 的理解。postById reducer 切片將工作委拖給 addComment,addComment 將新的評論 id 插入到相應的數據項中。同時 commentsById 和 allComments 的 reducer 切片都有自己的 case reducer,他們更新評論查找表和所有評論 id 列表的表。
## 其他方法
### 基于任務的更新
reducer 僅僅是個函數,因此有無數種方法來拆分這個邏輯。使用切片 reducer 是最常見,但也可以在更面向任務的結構中組織行為。由于通常會涉及到更多嵌套的更新,因此常常會使用 [dot-prop-immutable](https://github.com/debitoor/dot-prop-immutable)、[object-path-immutable](https://github.com/mariocasciaro/object-path-immutable) 等庫實現不可變更新。
```javascript
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_COMMENT` 這個 case 要干哪些事更加清楚,但需要更新嵌套邏輯和對特定狀態樹的了解。最后這取決于你如何組織 reducer 邏輯,或許你根本不需要這樣做。
### Redux-ORM
[Redux-ORM](https://github.com/tommikaikkonen/redux-orm) 庫提供了一個非常有用的抽象層,用于管理 Redux store 中存儲的范式化數據。它允許你聲明 Model 類并且定義他們之間的關系。然后它可以為你的數據類型生成新“表”,充當用于查找數據的特殊選擇器工具,并且對數據執行不可變更新。
有幾種方法可以用 Redux-ORM 執行數據更新。首選,Redux-ORM 文檔建議在每個 Model 子類上定義 reducer 函數,然后將自動生成的組合 reducer 函數放到 store 中:
```javascript
// models.js
import { Model, many, Schema } from 'redux-orm'
export class Post extends Model {
static get fields() {
return {
// 定義一個多邊關系 - 一個 Post 可以有多個 Comments,
// 字段名是 “comments”
comments: many('Comment')
}
}
static reducer(state, action, Post) {
switch (action.type) {
case 'CREATE_POST': {
// 排隊創建一個 Post 實例
Post.create(action.payload)
break
}
case 'ADD_COMMENT': {
const { payload } = action
const { postId, commentId } = payload
// 排隊增加一個 Comment ID 和 Post 實例的聯系
Post.withId(postId).comments.add(commentId)
break
}
}
// Redux-ORM 將在返回后自動應用排隊的更新
}
}
Post.modelName = 'Post'
export class Comment extends Model {
static get fields() {
return {}
}
static reducer(state, action, Comment) {
switch (action.type) {
case 'ADD_COMMENT': {
const { payload } = action
const { commentId, commentText } = payload
// 排隊創建一個 Comment 實例
Comment.create({ id: commentId, text: commentText })
break
}
}
// Redux-ORM 將在返回后自動應用排隊的更新
}
}
Comment.modelName = 'Comment'
// 創建 Schema 實例,然后和 Post、Comment 數據模型掛鉤起來
export const schema = new Schema()
schema.register(Post, Comment)
// main.js
import { createStore, combineReducers } from 'redux'
import { schema } from './models'
const rootReducer = combineReducers({
// 插入 Redux-ORM 自動生成的 reducer,這將
// 初始化一個數據模型 “表”,并且和我們在
// 每個 Model 子類中定義的 reducer 邏輯掛鉤起來
entities: schema.reducer()
})
// dispatch 一個 action 以創建一個 Post 實例
store.dispatch({
type: 'CREATE_POST',
payload: {
id: 1,
name: 'Test Post Please Ignore'
}
})
// dispath 一個 action 以創建一個 Comment 實例作為上個 Post 的子元素
store.dispatch({
type: 'ADD_COMMENT',
payload: {
postId: 1,
commentId: 123,
commentText: 'This is a comment'
}
})
```
Redux-ORM 庫維護要應用的內部更新隊列。這些更新是不可變更新,這個庫簡化了這個更新過程。
使用 Redux-ORM 的另一個變化是用一個單一的 case reducer 作為抽象層。
```javascript
import { schema } from './models'
// 假設這個 case reducer 正在我們的 “entities” 切片 reducer 使用,
// 并且我們在 Redux-ORM 的 Model 子類上沒有定義 reducer
function addComment(entitiesState, action) {
const session = schema.from(entitiesState)
const { Post, Comment } = session
const { payload } = action
const { postId, commentId, commentText } = payload
const post = Post.withId(postId)
post.comments.add(commentId)
Comment.create({ id: commentId, text: commentText })
return session.reduce()
}
```
總之,Redux-ORM 提供了一組非常有用的抽象,用于定義數據類型之間的關系,在我們的 state 中創建了一個 “表”,檢索和反規劃關系數據,以及將不可變更新應用于關系數據。
- 自述
- 介紹
- 動機
- 核心概念
- 三大原則
- 先前技術
- 學習資源
- 生態系統
- 示例
- 基礎
- Action
- Reducer
- Store
- 數據流
- 搭配 React
- 示例:Todo List
- 高級
- 異步 Action
- 異步數據流
- Middleware
- 搭配 React Router
- 示例:Reddit API
- 下一步
- 技巧
- 配置 Store
- 遷移到 Redux
- 使用對象展開運算符
- 減少樣板代碼
- 服務端渲染
- 編寫測試
- 計算衍生數據
- 實現撤銷重做
- 子應用隔離
- 組織 Reducer
- Reducer 基礎概念
- Reducer 基礎結構
- Reducer 邏輯拆分
- Reducer 重構示例
- combineReducers 用法
- combineReducers 進階
- State 范式化
- 管理范式化數據
- Reducer 邏輯復用
- 不可變更新模式
- 初始化 State
- 結合 Immutable.JS 使用 Redux
- 常見問題
- 綜合
- Reducer
- 組織 State
- 創建 Store
- Action
- 不可變數據
- 代碼結構
- 性能
- 設計哲學
- React Redux
- 其它
- 排錯
- 詞匯表
- API 文檔
- createStore
- Store
- combineReducers
- applyMiddleware
- bindActionCreators
- compose
- react-redux 文檔
- API
- 排錯