使用 combineReducers
核心概念
Redux 應用程式最常見的狀態形狀是純 Javascript 物件,其中包含每個頂層金鑰的特定於網域的資料「切片」。類似地,針對該狀態形狀撰寫 reducer 邏輯最常見的方法是使用「切片 reducer」函式,每個函式都具有相同的 (state, action)
簽章,並且每個函式都負責管理對該特定狀態切片的更新。多個切片 reducer 可以回應相同的動作,視需要獨立更新自己的切片,並且更新的切片會合併到新的狀態物件中。
由於這種模式非常常見,因此 Redux 提供 combineReducers
實用程式來實作該行為。這是高階 reducer 的範例,它會取得一個包含切片 reducer 函式的物件,並傳回新的 reducer 函式。
使用 combineReducers
時,有幾個重要的觀念需要了解
- 首先也是最重要的,
combineReducers
只是一個 用來簡化撰寫 Redux 函數時最常見使用案例的工具函數。你不需要在自己的應用程式中使用它,而且它並不會處理所有可能發生的情況。完全有可能在不使用它的情況下撰寫函數邏輯,而且在combineReducer
無法處理的情況下,需要撰寫自訂函數邏輯是很常見的。(請參閱 超越combineReducers
以取得範例和建議。) - 雖然 Redux 本身對於你的狀態如何組織並沒有意見,但
combineReducers
執行多項規則,以協助使用者避免常見的錯誤。(請參閱combineReducers
以取得詳細資訊。) - 一個經常被問到的問題是 Redux 在發送動作時是否「呼叫所有函數」。由於實際上只有一個根函數函數,因此預設答案是「否,它不會」。不過,
combineReducers
有特定的行為會這樣做。為了組裝新的狀態樹,combineReducers
會使用其目前的狀態區塊和目前的動作呼叫每個區塊函數,讓區塊函數有機會回應並在需要時更新其狀態區塊。因此,在這個意義上,使用combineReducers
會「呼叫所有函數」,或至少會呼叫它封裝的所有區塊函數。 - 你可以在函數結構的所有層級使用它,而不仅仅是用來建立根函數。在不同的地方有多個合併函數是很常見的,這些函數會組合在一起以建立根函數。
定義狀態形狀
有兩種方式可以定義商店狀態的初始形狀和內容。首先,createStore
函數可以將 preloadedState
作為其第二個引數。這主要是為了使用先前儲存在其他地方的狀態(例如瀏覽器的 localStorage)來初始化商店。另一種方式是,當狀態引數為 undefined
時,根函數會傳回初始狀態值。這兩種方法在 初始化狀態 中有更詳細的說明,但在使用 combineReducers
時,還有一些額外的問題需要注意。
combineReducers
會取得一個包含多個區塊 reducer 函式的物件,並建立一個輸出對應狀態物件的函式,其鍵值與輸入物件相同。這表示如果未提供預載入狀態給 createStore
,輸入區塊 reducer 物件中鍵值的命名將定義輸出狀態物件中鍵值的命名。這些名稱之間的關聯性並不總是顯而易見,特別是在使用預設模組匯出和物件文字速記等功能時。
以下範例說明如何使用 combineReducers
中的物件文字速記來定義狀態形狀
// reducers.js
export default theDefaultReducer = (state = 0, action) => state
export const firstNamedReducer = (state = 1, action) => state
export const secondNamedReducer = (state = 2, action) => state
// rootReducer.js
import { combineReducers, createStore } from 'redux'
import theDefaultReducer, {
firstNamedReducer,
secondNamedReducer
} from './reducers'
// Use object literal shorthand syntax to define the object shape
const rootReducer = combineReducers({
theDefaultReducer,
firstNamedReducer,
secondNamedReducer
})
const store = createStore(rootReducer)
console.log(store.getState())
// {theDefaultReducer : 0, firstNamedReducer : 1, secondNamedReducer : 2}
請注意,由於我們使用物件文字速記來定義物件,因此結果狀態中的鍵值與匯入變數名稱相同。這可能並非總是預期的行為,而且對於不熟悉現代 JS 語法的人來說,這通常會造成混淆。
此外,結果名稱有點奇怪。在狀態鍵值中包含「reducer」等字詞通常不是一個好習慣 - 鍵值應僅反映其所包含資料的網域或類型。這表示我們應明確指定區塊 reducer 物件中鍵值的名稱以定義輸出狀態物件中的鍵值,或在使用物件文字速記語法時,小心重新命名匯入區塊 reducer 的變數以設定鍵值。
更好的用法可能是
import { combineReducers, createStore } from 'redux'
// Rename the default import to whatever name we want. We can also rename a named import.
import defaultState, {
firstNamedReducer,
secondNamedReducer as secondState
} from './reducers'
const rootReducer = combineReducers({
defaultState, // key name same as the carefully renamed default export
firstState: firstNamedReducer, // specific key name instead of the variable name
secondState // key name same as the carefully renamed named export
})
const reducerInitializedStore = createStore(rootReducer)
console.log(reducerInitializedStore.getState())
// {defaultState : 0, firstState : 1, secondState : 2}
此狀態形狀更能反映所涉及的資料,因為我們小心設定傳遞給 combineReducers
的鍵值。