使用函式分解和 Reducer 組合重構 Reducer 邏輯
查看不同類型子 Reducer 函式的範例及其組合方式可能有所助益。讓我們來看看如何將大型單一 Reducer 函式重構成由多個較小型函式組成的組合。
注意:此範例刻意採用冗長風格,目的是為了說明重構的概念和流程,而非編寫簡潔的程式碼。
初始 Reducer
假設我們的初始 Reducer 如下所示
const initialState = {
visibilityFilter: 'SHOW_ALL',
todos: []
}
function appReducer(state = initialState, action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER': {
return Object.assign({}, state, {
visibilityFilter: action.filter
})
}
case 'ADD_TODO': {
return Object.assign({}, state, {
todos: state.todos.concat({
id: action.id,
text: action.text,
completed: false
})
})
}
case 'TOGGLE_TODO': {
return Object.assign({}, state, {
todos: state.todos.map(todo => {
if (todo.id !== action.id) {
return todo
}
return Object.assign({}, todo, {
completed: !todo.completed
})
})
})
}
case 'EDIT_TODO': {
return Object.assign({}, state, {
todos: state.todos.map(todo => {
if (todo.id !== action.id) {
return todo
}
return Object.assign({}, todo, {
text: action.text
})
})
})
}
default:
return state
}
}
該函式相當簡短,但已經變得過於複雜。我們處理了兩個不同的關注領域(過濾與管理待辦事項清單),巢狀結構讓更新邏輯更難以閱讀,而且並非每個地方都很清楚在發生什麼事。
萃取公用函式
一個好的第一步可能是建立一個公用函式,以傳回具有更新欄位的物件。此外,我們可以萃取一個函式來更新陣列中的特定項目,因為這是一個重複的模式
function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues)
}
function updateItemInArray(array, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if (item.id !== itemId) {
// Since we only want to update one item, preserve all others as they are now
return item
}
// Use the provided callback to create an updated item
const updatedItem = updateItemCallback(item)
return updatedItem
})
return updatedItems
}
function appReducer(state = initialState, action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER': {
return updateObject(state, { visibilityFilter: action.filter })
}
case 'ADD_TODO': {
const newTodos = state.todos.concat({
id: action.id,
text: action.text,
completed: false
})
return updateObject(state, { todos: newTodos })
}
case 'TOGGLE_TODO': {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, { completed: !todo.completed })
})
return updateObject(state, { todos: newTodos })
}
case 'EDIT_TODO': {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, { text: action.text })
})
return updateObject(state, { todos: newTodos })
}
default:
return state
}
}
這減少了重複,也讓程式碼更容易閱讀。
萃取案例 Reducer
接下來,我們可以將每個特定案例拆分為自己的函式
// Omitted
function updateObject(oldObject, newValues) {}
function updateItemInArray(array, itemId, updateItemCallback) {}
function setVisibilityFilter(state, action) {
return updateObject(state, { visibilityFilter: action.filter })
}
function addTodo(state, action) {
const newTodos = state.todos.concat({
id: action.id,
text: action.text,
completed: false
})
return updateObject(state, { todos: newTodos })
}
function toggleTodo(state, action) {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, { completed: !todo.completed })
})
return updateObject(state, { todos: newTodos })
}
function editTodo(state, action) {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, { text: action.text })
})
return updateObject(state, { todos: newTodos })
}
function appReducer(state = initialState, action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return setVisibilityFilter(state, action)
case 'ADD_TODO':
return addTodo(state, action)
case 'TOGGLE_TODO':
return toggleTodo(state, action)
case 'EDIT_TODO':
return editTodo(state, action)
default:
return state
}
}
現在,每個案例中發生的事都非常清楚。我們也可以開始看到一些模式浮現。
依據網域區分資料處理
我們的應用程式 Reducer 仍然知道應用程式的所有不同案例。讓我們嘗試將其拆分,以便將過濾邏輯和待辦事項邏輯分開
// Omitted
function updateObject(oldObject, newValues) {}
function updateItemInArray(array, itemId, updateItemCallback) {}
function setVisibilityFilter(visibilityState, action) {
// Technically, we don't even care about the previous state
return action.filter
}
function visibilityReducer(visibilityState = 'SHOW_ALL', action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return setVisibilityFilter(visibilityState, action)
default:
return visibilityState
}
}
function addTodo(todosState, action) {
const newTodos = todosState.concat({
id: action.id,
text: action.text,
completed: false
})
return newTodos
}
function toggleTodo(todosState, action) {
const newTodos = updateItemInArray(todosState, action.id, todo => {
return updateObject(todo, { completed: !todo.completed })
})
return newTodos
}
function editTodo(todosState, action) {
const newTodos = updateItemInArray(todosState, action.id, todo => {
return updateObject(todo, { text: action.text })
})
return newTodos
}
function todosReducer(todosState = [], action) {
switch (action.type) {
case 'ADD_TODO':
return addTodo(todosState, action)
case 'TOGGLE_TODO':
return toggleTodo(todosState, action)
case 'EDIT_TODO':
return editTodo(todosState, action)
default:
return todosState
}
}
function appReducer(state = initialState, action) {
return {
todos: todosReducer(state.todos, action),
visibilityFilter: visibilityReducer(state.visibilityFilter, action)
}
}
請注意,由於兩個「狀態區段」Reducer 現在只取得整個狀態中屬於自己的部分作為參數,因此不再需要傳回複雜的巢狀狀態物件,也因此變得更為簡潔。
減少樣板程式碼
我們幾乎完成了。由於許多人不喜歡 switch 陳述式,因此使用建立動作類型至案例函數的查詢表的函數非常常見。我們將使用 減少樣板程式碼 中所述的 createReducer
函數
// Omitted
function updateObject(oldObject, newValues) {}
function updateItemInArray(array, itemId, updateItemCallback) {}
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}
// Omitted
function setVisibilityFilter(visibilityState, action) {}
const visibilityReducer = createReducer('SHOW_ALL', {
SET_VISIBILITY_FILTER: setVisibilityFilter
})
// Omitted
function addTodo(todosState, action) {}
function toggleTodo(todosState, action) {}
function editTodo(todosState, action) {}
const todosReducer = createReducer([], {
ADD_TODO: addTodo,
TOGGLE_TODO: toggleTodo,
EDIT_TODO: editTodo
})
function appReducer(state = initialState, action) {
return {
todos: todosReducer(state.todos, action),
visibilityFilter: visibilityReducer(state.visibilityFilter, action)
}
}
按區塊合併 Reducer
作為我們的最後一步,我們現在可以使用 Redux 內建的 combineReducers
實用程式來處理頂層應用程式 Reducer 的「狀態區塊」邏輯。以下是最終結果
// Reusable utility functions
function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues)
}
function updateItemInArray(array, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if (item.id !== itemId) {
// Since we only want to update one item, preserve all others as they are now
return item
}
// Use the provided callback to create an updated item
const updatedItem = updateItemCallback(item)
return updatedItem
})
return updatedItems
}
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}
// Handler for a specific case ("case reducer")
function setVisibilityFilter(visibilityState, action) {
// Technically, we don't even care about the previous state
return action.filter
}
// Handler for an entire slice of state ("slice reducer")
const visibilityReducer = createReducer('SHOW_ALL', {
SET_VISIBILITY_FILTER: setVisibilityFilter
})
// Case reducer
function addTodo(todosState, action) {
const newTodos = todosState.concat({
id: action.id,
text: action.text,
completed: false
})
return newTodos
}
// Case reducer
function toggleTodo(todosState, action) {
const newTodos = updateItemInArray(todosState, action.id, todo => {
return updateObject(todo, { completed: !todo.completed })
})
return newTodos
}
// Case reducer
function editTodo(todosState, action) {
const newTodos = updateItemInArray(todosState, action.id, todo => {
return updateObject(todo, { text: action.text })
})
return newTodos
}
// Slice reducer
const todosReducer = createReducer([], {
ADD_TODO: addTodo,
TOGGLE_TODO: toggleTodo,
EDIT_TODO: editTodo
})
// "Root reducer"
const appReducer = combineReducers({
visibilityFilter: visibilityReducer,
todos: todosReducer
})
我們現在有幾種分割 Reducer 函數的範例:像 updateObject
和 createReducer
這樣的輔助程式,像 setVisibilityFilter
和 addTodo
這樣的特定案例處理常式,以及像 visibilityReducer
和 todosReducer
這樣的狀態區塊處理常式。我們也可以看到 appReducer
是「根 Reducer」的範例。
雖然此範例中的最終結果明顯比原始版本長,但這主要是由於提取了實用程式函數、增加了註解,以及為了清楚起見而進行了一些故意的冗長說明,例如分開的 return 陳述式。個別檢視每個函數,責任量現在較小,而且意圖也比較清楚。此外,在實際應用程式中,這些函數可能會被分割成不同的檔案,例如 reducerUtilities.js
、visibilityReducer.js
、todosReducer.js
和 rootReducer.js
。