Using combineReducers: Explanations of how combineReducers works in practice">Using combineReducers: Explanations of how combineReducers works in practice">
跳至主要內容

使用 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 的鍵值。