跳至主要內容

Redux 基礎知識,第 3 部分:狀態、動作和簡化器

Redux 基礎知識,第 3 部分:狀態、動作和簡化器

您將學到什麼
  • 如何定義包含應用程式資料的狀態值
  • 如何定義描述應用程式中發生事件的動作物件
  • 如何撰寫根據現有狀態和動作計算更新狀態的還原函數
先備條件

簡介

第 2 部分:Redux 概念和資料流程中,我們探討了 Redux 如何透過提供一個集中放置全域應用程式狀態的單一位置,來協助我們建構可維護的應用程式。我們也討論了 Redux 的核心概念,例如發送動作物件和使用會傳回新狀態值的還原函數。

現在您對這些部分有些概念後,是時候將這些知識付諸實行了。我們將建構一個小型範例應用程式,以了解這些部分實際上如何共同運作。

注意

請注意,本教學課程故意顯示較舊的 Redux 邏輯模式,這些模式比我們教導作為建構 Redux 應用程式的正確方法的 Redux Toolkit 的「現代 Redux」模式需要更多程式碼,目的是說明 Redux 背後的原則和概念。它並非旨在成為可供實際使用的專案。

請參閱下列網頁,以了解如何使用 Redux Toolkit 的「現代 Redux」

專案設定

針對本教學課程,我們建立了一個預先設定的入門專案,其中已設定 React,包含一些預設樣式,並具備一個虛擬 REST API,讓我們可以在應用程式中撰寫實際的 API 要求。您將使用此專案作為撰寫實際應用程式程式碼的基礎。

若要開始,您可以開啟並複製此 CodeSandbox

您也可以從此 Github 存放庫複製相同的專案。複製存放庫後,您可以使用 npm install 安裝專案工具,並使用 npm start 啟動專案。

如果您想查看我們將建置的最終版本,您可以查看tutorial-steps 分支,或在此 CodeSandbox 中查看最終版本

建立新的 Redux + React 專案

完成本教學課程後,您可能會想嘗試處理自己的專案。我們建議使用Redux 模板建立 Create-React-App,這是建立新的 Redux + React 專案的最快方式。它已設定好 Redux Toolkit 和 React-Redux,使用第 1 部分中「計數器」應用程式範例的現代化版本。這讓您可以直接撰寫實際的應用程式程式碼,而無需新增 Redux 套件和設定儲存庫。

如果您想了解將 Redux 新增到專案的具體詳細資訊,請參閱此說明

詳細說明:將 Redux 新增到 React 專案

Redux 模板建立 CRA 已設定好 Redux Toolkit 和 React-Redux。如果您從頭開始設定新的專案,請遵循下列步驟

  • 新增 @reduxjs/toolkitreact-redux 套件
  • 使用 RTK 的 configureStore API 建立 Redux store,並傳入至少一個 reducer 函式
  • 將 Redux store 匯入應用程式的進入點檔案(例如 src/index.js
  • 使用 React-Redux 的 <Provider> 元件包裝根 React 元件,如下所示
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

探索初始專案

此初始專案基於 標準 Create-React-App 專案範本,並進行一些修改。

讓我們快速了解初始專案包含的內容

  • /src
    • index.js:應用程式的進入點檔案。它會呈現主要的 <App> 元件。
    • App.js:主要的應用程式元件。
    • index.css:完整應用程式的樣式
    • /api
      • client.js:一個小型 AJAX 要求客戶端,允許我們進行 GET 和 POST 要求
      • server.js:為我們的資料提供一個假的 REST API。我們的應用程式稍後會從這些假的端點擷取資料。
    • /exampleAddons:包含一些額外的 Redux 外掛程式,我們將在稍後的教學中使用它們來展示運作方式

如果您現在載入應用程式,您應該會看到歡迎訊息,但應用程式的其他部分都是空的。

讓我們開始吧!

開始 Todo 範例應用程式

我們的範例應用程式將是一個小型「待辦事項」應用程式。您可能以前看過待辦事項應用程式的範例 - 它們是很好的範例,因為它們讓我們展示如何執行追蹤項目清單、處理使用者輸入,以及在資料變更時更新 UI 等事項,這些都是正常應用程式中會發生的所有事項。

定義需求

讓我們從找出此應用程式的初始業務需求開始

  • UI 應包含三個主要區段
    • 一個輸入方塊,讓使用者輸入新待辦事項的文字
    • 所有現有待辦事項的清單
    • 一個頁尾區段,顯示未完成待辦事項的數量,並顯示篩選選項
  • 待辦事項清單項目應有一個核取方塊,用於切換其「已完成」狀態。我們還應該能夠為預先定義的顏色清單新增一個顏色編碼的類別標籤,並刪除待辦事項。
  • 計數器應將活動待辦事項的數量複數化:「0 個項目」、「1 個項目」、「3 個項目」等
  • 應有按鈕將所有待辦事項標記為已完成,並清除所有已完成的待辦事項
  • 顯示在清單中的待辦事項應該有兩種篩選方式
    • 根據顯示「全部」、「進行中」和「已完成」待辦事項進行篩選
    • 根據選擇一種或多種顏色,並顯示標籤與這些顏色相符的任何待辦事項進行篩選

我們稍後會增加一些其他需求,但這足以讓我們開始。

最終目標是應用程式看起來像這樣

Example todo app screenshot

設計狀態值

React 和 Redux 的核心原則之一是您的 UI 應基於您的狀態。因此,設計應用程式的其中一種方法是先考慮描述應用程式如何運作所需的所有狀態。盡可能使用狀態中的較少值來描述您的 UI 也是一個好主意,這樣您需要追蹤和更新的資料就較少。

在概念上,此應用程式有兩個主要面向

  • 目前待辦事項的實際清單
  • 目前的篩選選項

我們還需要追蹤使用者在「新增待辦事項」輸入方塊中輸入的資料,但這不那麼重要,我們稍後會處理。

對於每個待辦事項,我們需要儲存一些資訊

  • 使用者輸入的文字
  • 布林旗標,表示是否已完成
  • 唯一的 ID 值
  • 如果已選取,則為顏色類別

我們的篩選行為可能可以用一些列舉值來描述

  • 完成狀態:「全部」、「進行中」和「已完成」
  • 顏色:「紅色」、「黃色」、「綠色」、「藍色」、「橘色」、「紫色」

檢視這些值,我們也可以說待辦事項是「應用程式狀態」(應用程式處理的核心資料),而篩選值是「UI 狀態」(描述應用程式目前正在執行什麼動作的狀態)。思考這些不同類型的類別有助於了解狀態的不同部分如何使用。

設計狀態結構

使用 Redux,我們的應用程式狀態始終儲存在純 JavaScript 物件和陣列中。這表示您可能無法將其他內容放入 Redux 狀態中,例如類別執行個體、內建 JS 類型(例如 Map / Set / Promise / Date)、函數或任何其他不是純 JS 資料的內容。

根 Redux 狀態值幾乎總是純 JS 物件,其他資料則嵌套在其中。

根據這些資訊,我們現在應該可以描述 Redux 狀態中我們需要擁有的值類型

  • 首先,我們需要一個待辦事項物件陣列。每個項目都應具有這些欄位
    • id:一個唯一數字
    • text:使用者輸入的文字
    • completed:一個布林旗標
    • color:一個選用色彩類別
  • 接著,我們需要描述我們的篩選選項。我們需要擁有
    • 目前的「已完成」篩選值
    • 目前選取的色彩類別陣列

因此,以下是我們應用程式狀態範例可能的外觀

const todoAppState = {
todos: [
{ id: 0, text: 'Learn React', completed: true },
{ id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
{ id: 2, text: 'Build something fun!', completed: false, color: 'blue' }
],
filters: {
status: 'Active',
colors: ['red', 'blue']
}
}

請務必注意,在 Redux 外部擁有其他狀態值是可以的!到目前為止,此範例夠小,因此我們實際上將所有狀態都放在 Redux 儲存體中,但正如我們稍後將看到的,有些資料真的不需要保存在 Redux 中(例如「此下拉式選單是否開啟?」或「表單輸入的目前值」)。

設計動作

動作是具有 type 欄位的純 JavaScript 物件。如前所述,你可以將動作視為一個事件,用來描述應用程式中發生的事情

就像我們根據應用程式的需求設計狀態結構一樣,我們也應該能夠提出一些動作清單,來說明正在發生的事情

  • 根據使用者輸入的文字新增一個新的待辦事項項目
  • 切換待辦事項的已完成狀態
  • 為待辦事項選擇一個色彩類別
  • 刪除一個待辦事項
  • 將所有待辦事項標示為已完成
  • 清除所有已完成的待辦事項
  • 選擇不同的「已完成」篩選值
  • 新增一個色彩篩選器
  • 移除一個色彩篩選器

我們通常會將任何描述正在發生的事情所需的額外資料放入 action.payload 欄位中。這可以是一個數字、一個字串或一個包含多個欄位的物件。

Redux 儲存體不關心 action.type 欄位的實際文字為何。但是,你自己的程式碼會查看 action.type 以查看是否需要更新。此外,在除錯時,你會經常在 Redux DevTools Extension 中查看動作類型字串,以查看應用程式中正在發生的事情。因此,請嘗試選擇可讀且清楚描述正在發生的事情的動作類型 - 以後查看時會更容易理解事情!

根據可以發生的事情清單,我們可以建立應用程式將使用的動作清單

  • {type: 'todos/todoAdded', payload: todoText}
  • {type: 'todos/todoToggled', payload: todoId}
  • {type: 'todos/colorSelected', payload: {todoId, color}}
  • {type: 'todos/todoDeleted', payload: todoId}
  • {type: 'todos/allCompleted'}
  • {type: 'todos/completedCleared'}
  • {type: 'filters/statusFilterChanged', payload: filterValue}
  • {type: 'filters/colorFilterChanged', payload: {color, changeType}}

在這種情況下,動作主要有一個額外的資料,因此我們可以直接將其放入 action.payload 欄位。我們可以將顏色篩選行為分成兩個動作,一個是「新增」,另一個是「移除」,但在此情況下,我們會將其作為一個動作,並在其中包含一個額外欄位,特別顯示我們可以將物件作為動作有效負載。

與狀態資料一樣,動作應包含描述發生事件所需的最小資訊量

撰寫 Reducer

現在我們知道狀態結構和動作的外觀,是時候撰寫第一個 Reducer 了。

Reducer 是將目前的 stateaction 作為引數,並傳回新的 state 結果的函式。換句話說,(state, action) => newState

建立 Root Reducer

Redux 應用程式實際上只有一個 Reducer 函式:您稍後會傳遞給 createStore 的「root reducer」函式。那個 root reducer 函式負責處理所有已發派的動作,並計算每次應為 全部 的新狀態結果。

讓我們先在 src 資料夾中建立一個 reducer.js 檔案,與 index.jsApp.js 並列。

每個 Reducer 都需要一些初始狀態,因此我們會加入一些假的待辦事項條目來開始。然後,我們可以撰寫 Reducer 函式內部邏輯的綱要

src/reducer.js
const initialState = {
todos: [
{ id: 0, text: 'Learn React', completed: true },
{ id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
{ id: 2, text: 'Build something fun!', completed: false, color: 'blue' }
],
filters: {
status: 'All',
colors: []
}
}

// Use the initialState as a default value
export default function appReducer(state = initialState, action) {
// The reducer normally looks at the action type field to decide what happens
switch (action.type) {
// Do something here based on the different types of actions
default:
// If this reducer doesn't recognize the action type, or doesn't
// care about this specific action, return the existing state unchanged
return state
}
}

在應用程式初始化時,Reducer 可能會以 undefined 作為狀態值呼叫。如果發生這種情況,我們需要提供初始狀態值,以便 Reducer 程式碼的其餘部分有東西可以使用。Reducer 通常使用預設引數語法提供初始狀態:(state = initialState, action)

接下來,讓我們加入處理 'todos/todoAdded' 動作的邏輯。

我們首先需要檢查目前動作的類型是否與那個特定字串相符。然後,我們需要傳回一個包含 全部 狀態的新物件,即使是那些沒有變更的欄位也是如此。

src/reducer.js
function nextTodoId(todos) {
const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1)
return maxId + 1
}

// Use the initialState as a default value
export default function appReducer(state = initialState, action) {
// The reducer normally looks at the action type field to decide what happens
switch (action.type) {
// Do something here based on the different types of actions
case 'todos/todoAdded': {
// We need to return a new state object
return {
// that has all the existing state data
...state,
// but has a new array for the `todos` field
todos: [
// with all of the old todos
...state.todos,
// and the new todo object
{
// Use an auto-incrementing numeric ID for this example
id: nextTodoId(state.todos),
text: action.payload,
completed: false
}
]
}
}
default:
// If this reducer doesn't recognize the action type, or doesn't
// care about this specific action, return the existing state unchanged
return state
}
}

這... 為了在狀態中新增一個待辦事項,需要做這麼多工作嗎?為什麼需要這麼多額外的步驟?

簡化器的規則

我們之前說過,簡化器永遠必須遵循一些特別的規則

  • 它們只能根據 stateaction 參數計算新的狀態值
  • 它們不被允許修改現有的 state。相反地,它們必須透過複製現有的 state 並對複製的值進行變更,來進行不可變更新
  • 它們不能執行任何非同步邏輯或其他「副作用」
提示

「副作用」是指任何在函式回傳值之外可見的狀態或行為變更。一些常見的副作用類型包括

  • 將值記錄到主控台
  • 儲存檔案
  • 設定非同步計時器
  • 發出 AJAX HTTP 請求
  • 修改函式外部存在的某些狀態,或變異函式的參數
  • 產生亂數或唯一的亂數 ID(例如 Math.random()Date.now()

任何遵循這些規則的函式也稱為「純」函式,即使它並未特別寫成簡化器函式。

但為什麼這些規則很重要?有幾個不同的原因

  • Redux 的目標之一是讓你的程式碼可預測。當函式的輸出僅從輸入參數計算時,就更容易了解該程式碼如何運作,並對其進行測試。
  • 另一方面,如果函式依賴於自身外部的變數,或隨機運作,你永遠不知道執行它會發生什麼事。
  • 如果函式修改其他值,包括其參數,這可能會以意外的方式改變應用程式的工作方式。這可能是錯誤的常見來源,例如「我更新了我的狀態,但現在我的 UI 沒有在它應該更新的時候更新!」
  • Redux DevTools 的某些功能取決於你的簡化器是否正確遵循這些規則

關於「不可變更新」的規則特別重要,值得進一步討論。

Reducer 和不可變更新

稍早,我們討論了「突變」(修改現有物件/陣列值) 和「不可變性」(將值視為無法變更的內容)。

危險

在 Redux 中,我們的 reducer 絕不 允許突變原始/目前的狀態值!

// ❌ Illegal - by default, this will mutate the state!
state.value = 123

在 Redux 中,您不得突變狀態的原因有以下幾個

  • 它會導致錯誤,例如 UI 無法正確更新以顯示最新值
  • 它會讓您更難理解狀態為何以及如何更新
  • 它會讓您更難撰寫測試
  • 它會破壞正確使用「時間旅行除錯」的能力
  • 它違背了 Redux 預期的精神和使用模式

因此,如果我們無法變更原始值,我們要如何傳回更新的狀態?

提示

Reducer 只可以製作原始值的副本,然後它們可以突變副本。

// ✅ This is safe, because we made a copy
return {
...state,
value: 123
}

我們已經看到,我們可以手動撰寫不可變更新,方法是使用 JavaScript 的陣列/物件擴散運算子以及其他傳回原始值副本的函式。

當資料是巢狀時,這會變得更困難。不可變更新的重要規則是,您必須複製每個需要更新的巢狀層級。

但是,如果您認為「手動撰寫不可變更新看起來很難記住並正確執行」... 是的,您說對了! :)

手動撰寫不可變更新邏輯確實很困難,而且在 reducer 中意外突變狀態是 Redux 使用者最常犯的錯誤

提示

在實際應用中,您不必手動撰寫這些複雜的巢狀不可變更新。在第 8 部分:使用 Redux Toolkit 的現代 Redux中,您將學習如何使用 Redux Toolkit 簡化在 reducer 中撰寫不可變更新邏輯。

處理其他動作

有鑑於此,讓我們新增幾個案例的 reducer 邏輯。首先,根據待辦事項的 ID 切換其 已完成 欄位

src/reducer.js
export default function appReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
return {
...state,
todos: [
...state.todos,
{
id: nextTodoId(state.todos),
text: action.payload,
completed: false
}
]
}
}
case 'todos/todoToggled': {
return {
// Again copy the entire state object
...state,
// This time, we need to make a copy of the old todos array
todos: state.todos.map(todo => {
// If this isn't the todo item we're looking for, leave it alone
if (todo.id !== action.payload) {
return todo
}

// We've found the todo that has to change. Return a copy:
return {
...todo,
// Flip the completed flag
completed: !todo.completed
}
})
}
}
default:
return state
}
}

而且由於我們一直專注於待辦事項狀態,讓我們也新增一個案例來處理「可見性選取已變更」動作

src/reducer.js
export default function appReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
return {
...state,
todos: [
...state.todos,
{
id: nextTodoId(state.todos),
text: action.payload,
completed: false
}
]
}
}
case 'todos/todoToggled': {
return {
...state,
todos: state.todos.map(todo => {
if (todo.id !== action.payload) {
return todo
}

return {
...todo,
completed: !todo.completed
}
})
}
}
case 'filters/statusFilterChanged': {
return {
// Copy the whole state
...state,
// Overwrite the filters value
filters: {
// copy the other filter fields
...state.filters,
// And replace the status field with the new value
status: action.payload
}
}
}
default:
return state
}
}

我們只處理了 3 個動作,但這已經有點長了。如果我們嘗試在此一 reducer 函式中處理每個動作,將很難閱讀所有內容。

這就是為什麼reducer 通常會分割成多個較小的 reducer 函式 - 讓 reducer 邏輯更易於理解和維護。

拆分 Reducer

作為此一部分,Redux Reducer 通常會根據 Redux 狀態中它們更新的部分來拆分。我們的待辦事項應用程式狀態目前有兩個頂層部分:state.todosstate.filters。因此,我們可以將大型根部 Reducer 函式拆分成兩個較小的 Reducer,一個是 todosReducer,另一個是 filtersReducer

那麼,這些拆分後的 Reducer 函式應該放在哪裡?

我們建議根據「功能」來整理你的 Redux 應用程式資料夾和檔案,也就是與你的應用程式特定概念或區域相關的程式碼。特定功能的 Redux 程式碼通常會寫成單一檔案,稱為「區塊」檔案,其中包含所有 Reducer 邏輯和所有與該部分應用程式狀態相關的動作程式碼。

因此,Redux 應用程式狀態特定部分的 Reducer 稱為「區塊 Reducer」。通常,某些動作物件會與特定區塊 Reducer 緊密相關,因此動作類型字串應該以該功能的名稱 (例如 'todos') 開頭,並描述發生的事件 (例如 'todoAdded'),組合成一個字串 ('todos/todoAdded')。

在我們的專案中,建立一個新的 features 資料夾,然後在其中建立一個 todos 資料夾。建立一個名為 todosSlice.js 的新檔案,讓我們將與待辦事項相關的初始狀態剪下並貼到這個檔案中

src/features/todos/todosSlice.js
const initialState = [
{ id: 0, text: 'Learn React', completed: true },
{ id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
{ id: 2, text: 'Build something fun!', completed: false, color: 'blue' }
]

function nextTodoId(todos) {
const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1)
return maxId + 1
}

export default function todosReducer(state = initialState, action) {
switch (action.type) {
default:
return state
}
}

現在我們可以複製更新待辦事項的邏輯。不過,這裡有一個重要的差異。這個檔案只需要更新與待辦事項相關的狀態,它不再是巢狀的!這是我們拆分 Reducer 的另一個原因。由於待辦事項狀態本身就是一個陣列,我們不必在此處複製外部根部狀態物件。這使得這個 Reducer 更容易閱讀。

這稱為Reducer 組合,這是建構 Redux 應用程式的基本模式。

以下是我們處理這些動作後更新後的 Reducer 樣貌

src/features/todos/todosSlice.js
export default function todosReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
// Can return just the new todos array - no extra object around it
return [
...state,
{
id: nextTodoId(state),
text: action.payload,
completed: false
}
]
}
case 'todos/todoToggled': {
return state.map(todo => {
if (todo.id !== action.payload) {
return todo
}

return {
...todo,
completed: !todo.completed
}
})
}
default:
return state
}
}

這樣短了一些,也更容易閱讀。

現在我們可以對可見性邏輯執行相同操作。建立 src/features/filters/filtersSlice.js,讓我們將所有與篩選器相關的程式碼移到那裡

src/features/filters/filtersSlice.js
const initialState = {
status: 'All',
colors: []
}

export default function filtersReducer(state = initialState, action) {
switch (action.type) {
case 'filters/statusFilterChanged': {
return {
// Again, one less level of nesting to copy
...state,
status: action.payload
}
}
default:
return state
}
}

我們仍然必須複製包含篩選器狀態的物件,但由於巢狀結構較少,因此更容易閱讀正在發生的事情。

資訊

為了讓此頁面更簡潔,我們將略過其他動作的 reducer 更新邏輯寫法。

請根據上述需求自行撰寫這些更新。

如果您遇到困難,請參閱此頁面結尾的 CodeSandbox,以取得這些 reducer 的完整實作。

合併 Reducer

我們現在有兩個獨立的切片檔案,每個檔案都有自己的切片 reducer 函式。但是,我們先前提到 Redux 儲存庫在建立時需要一個根 reducer 函式。那麼,我們如何才能回到一個根 reducer,而不將所有程式碼都放在一個龐大的函式中呢?

由於 reducer 是正常的 JS 函式,我們可以將切片 reducer 匯入回 reducer.js,並撰寫一個新的根 reducer,其唯一的工作就是呼叫其他兩個函式。

src/reducer.js
import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

export default function rootReducer(state = {}, action) {
// always return a new object for the root state
return {
// the value of `state.todos` is whatever the todos reducer returns
todos: todosReducer(state.todos, action),
// For both reducers, we only pass in their slice of the state
filters: filtersReducer(state.filters, action)
}
}

請注意,這些 reducer 各自管理著全域狀態的不同部分。每個 reducer 的 state 參數都不同,且對應於它管理的狀態部分。

這讓我們可以根據功能和狀態切片來分割我們的邏輯,以保持易於維護性。

combineReducers

我們可以看到,新的根 reducer 對每個切片都執行相同的動作:呼叫切片 reducer,傳入由該 reducer 持有的狀態切片,並將結果指派回根狀態物件。如果我們要新增更多切片,模式將會重複。

Redux 核心函式庫包含一個名為 combineReducers 的工具程式,它為我們執行相同的樣板步驟。我們可以使用由 combineReducers 產生的較短程式碼取代我們手寫的 rootReducer

現在我們需要 combineReducers,因此是時候實際安裝 Redux 核心函式庫了:

npm install redux

完成後,我們可以匯入 combineReducers 並使用它

src/reducer.js
import { combineReducers } from 'redux'

import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

const rootReducer = combineReducers({
// Define a top-level state field named `todos`, handled by `todosReducer`
todos: todosReducer,
filters: filtersReducer
})

export default rootReducer

combineReducers 接受一個物件,其金鑰名稱將成為根狀態物件的金鑰,而其值是知道如何更新 Redux 狀態切片的切片 reducer 函式。

請記住,您提供給 combineReducers 的金鑰名稱決定了您的狀態物件的金鑰名稱!

您已學到什麼

狀態、動作和 Reducer 是 Redux 的建構區塊。每個 Redux 應用程式都有狀態值,建立動作來描述發生了什麼事,並使用 reducer 函式根據前一個狀態和動作計算新的狀態值。

以下是我們目前應用程式的內容

摘要
  • Redux 應用程式使用純粹的 JS 物件、陣列和基本型別作為狀態值
    • 根狀態值應為純粹的 JS 物件
    • 狀態應包含使應用程式運作所需的最小資料量
    • 類別、Promise、函式和其他非純粹值不應放入 Redux 狀態
    • Reducer 不得建立隨機值,例如 Math.random()Date.now()
    • 與 Redux 並存的其他狀態值(例如區域元件狀態)不在 Redux 儲存區中,這沒問題
  • 動作是具有描述發生事件的 type 欄位的純粹物件
    • type 欄位應為可讀的字串,通常寫成 'feature/eventName'
    • 動作可能包含其他值,這些值通常儲存在 action.payload 欄位中
    • 動作應具有描述發生事件所需的最小資料量
  • Reducer 是類似 (state, action) => newState 的函式
    • Reducer 必須始終遵循特殊規則
      • 僅根據 stateaction 引數計算新狀態
      • 絕不變異現有的 state - 始終傳回副本
      • 沒有「副作用」,例如 AJAX 呼叫或非同步邏輯
  • Reducer 應分開,以便更易於閱讀
    • Reducer 通常根據頂層狀態鍵或狀態「區塊」進行分開
    • Reducer 通常寫在「區塊」檔案中,並整理到「功能」資料夾中
    • Reducer 可以與 Redux combineReducers 函式結合在一起
    • 傳遞給 combineReducers 的鍵名稱定義頂層狀態物件鍵

下一步?

我們現在有一些 Reducer 邏輯將更新我們的狀態,但這些 Reducer 本身不會執行任何操作。它們需要放入 Redux 儲存區中,儲存區可以在發生某事時使用動作呼叫 Reducer 程式碼。

第 4 部分:儲存區 中,我們將瞭解如何建立 Redux 儲存區並執行我們的 Reducer 邏輯。