Redux 精華,第 1 部分:Redux 概觀和概念
- Redux 是什麼,以及您可能想要使用它的原因
- Redux 的關鍵術語和概念
- 資料如何流經 Redux 應用程式
簡介
歡迎來到 Redux Essentials 教學課程!本教學課程將介紹 Redux,並教導您如何使用它,使用我們最新推薦的工具和最佳實務。完成本課程後,您應該能夠使用在此學到的工具和模式開始建置自己的 Redux 應用程式。
在本教學課程的第一部分,我們將介紹使用 Redux 所需了解的主要概念和術語,而在 第二部分:Redux 應用程式結構 中,我們將探討一個基本的 React + Redux 應用程式,了解各部分如何組合在一起。
從 第三部分:基本的 Redux 資料流 開始,我們將使用這些知識建置一個具備一些實際功能的小型社群媒體饋送應用程式,了解這些部分實際上如何運作,並討論使用 Redux 的一些重要模式和準則。
如何閱讀本教學課程
本頁面將專注於向您展示如何正確使用 Redux,並僅說明足夠的概念,讓您了解如何正確建置 Redux 應用程式。
我們已盡量讓這些說明適合初學者,但我們確實需要對您已知的內容做出一些假設
- 熟悉 HTML 和 CSS。
- 熟悉 ES2015 語法和功能
- 了解 React 術語:JSX、狀態、函式元件、道具 和 Hook
- 了解 非同步 JavaScript 和 發出 AJAX 請求
如果您對這些主題還不熟悉,我們建議您先花點時間熟悉它們,然後再回來了解 Redux。當您準備好時,我們會在這裡等您!
您應該確保已在瀏覽器中安裝 React 和 Redux DevTools 擴充功能
- React DevTools 擴充功能
- Redux DevTools 擴充功能
什麼是 Redux?
首先,了解這個「Redux」是什麼很重要。它的功能是什麼?它能幫我解決什麼問題?為什麼我要使用它?
Redux 是一種模式和函式庫,用於管理和更新應用程式狀態,使用稱為「動作」的事件。它作為一個集中式儲存庫,儲存整個應用程式中需要使用的狀態,並透過規則確保只能以可預測的方式更新狀態。
我為什麼應該使用 Redux?
Redux 協助您管理「全域」狀態,也就是應用程式許多部分中需要的狀態。
Redux 提供的模式和工具,讓您更容易了解應用程式中的狀態何時、何地、為何以及如何更新,以及當這些變更發生時,應用程式邏輯將如何運作。Redux 引導您撰寫可預測且可測試的程式碼,這有助於讓您確信應用程式會按照預期運作。
我什麼時候應該使用 Redux?
Redux 協助您處理共用狀態管理,但就像任何工具一樣,它有其取捨。有更多概念要學習,還有更多程式碼要撰寫。它也為您的程式碼增加了一些間接性,並要求您遵循某些限制。這是短期和長期生產力之間的取捨。
在以下情況下,Redux 更為實用
- 您有大量的應用程式狀態,需要在應用程式的許多地方使用
- 應用程式狀態會隨著時間頻繁更新
- 更新該狀態的邏輯可能很複雜
- 應用程式有中型或大型程式碼庫,而且可能由許多人共同開發
並非所有應用程式都需要 Redux。花點時間思考您正在建構的應用程式類型,並決定哪些工具最能幫助您解決您正在處理的問題。
如果您不確定 Redux 是否是您應用程式的理想選擇,這些資源提供了一些進一步的指導
Redux 函式庫與工具
Redux 是一個小型獨立的 JS 函式庫。然而,它通常與其他幾個套件一起使用
React-Redux
Redux 可以與任何 UI 架構整合,最常與 React 一起使用。 React-Redux 是我們的官方套件,讓你的 React 元件可以透過讀取狀態片段和發送動作來更新儲存庫,與 Redux 儲存庫互動。
Redux Toolkit
Redux Toolkit 是我們建議撰寫 Redux 邏輯的方法。它包含我們認為對建構 Redux 應用程式至關重要的套件和函式。Redux Toolkit 建構在我們的建議最佳實務中,簡化大多數 Redux 任務,防止常見錯誤,並讓撰寫 Redux 應用程式變得更容易。
Redux DevTools 擴充功能
Redux DevTools 擴充功能 會顯示 Redux 儲存庫中隨著時間推移的狀態變更記錄。這讓你能夠有效除錯你的應用程式,包括使用「時間旅行除錯」等強大技術。
Redux 術語與概念
在我們深入探討實際程式碼之前,讓我們來討論一些使用 Redux 所需了解的術語與概念。
狀態管理
讓我們先從一個小型 React 計數器元件開始。它追蹤元件狀態中的數字,並在按鈕被按一下時遞增該數字
function Counter() {
// State: a counter value
const [counter, setCounter] = useState(0)
// Action: code that causes an update to the state when something happens
const increment = () => {
setCounter(prevCounter => prevCounter + 1)
}
// View: the UI definition
return (
<div>
Value: {counter} <button onClick={increment}>Increment</button>
</div>
)
}
它是一個包含以下部分的獨立應用程式
- 狀態,驅動我們應用程式的真實來源;
- 檢視,基於目前狀態的 UI 宣告式描述
- 動作,基於使用者輸入而在應用程式中發生的事件,並觸發狀態更新
這是「單向資料流」的一個小範例
- 狀態描述應用程式在特定時間點的條件
- 使用者介面根據該狀態進行呈現
- 當某件事發生(例如使用者按一下按鈕)時,狀態會根據發生的事情進行更新
- 使用者介面根據新的狀態重新呈現
但是,當我們有多個需要共用和使用相同狀態的元件時,這種簡單性可能會失效,特別是如果這些元件位於應用程式的不同部分時。有時這可以用 「提升狀態」 到父元件來解決,但這並不總是能提供幫助。
解決此問題的方法之一是從元件中擷取共用狀態,並將其放入元件樹外部的集中位置。這樣,我們的元件樹就會變成一個大的「檢視」,而且任何元件都可以存取狀態或觸發動作,無論它們在樹中的哪個位置!
透過定義和區分狀態管理中涉及的概念,並強制執行維持檢視和狀態之間獨立性的規則,我們可以讓程式碼更具結構性和可維護性。
這是 Redux 背後的基本概念:一個單一的集中位置,用於包含應用程式中的全域狀態,以及在更新該狀態時要遵循的特定模式,以使程式碼可預測。
不可變性
「可變」表示「可變更」。如果某個東西是「不可變」的,那麼它永遠不會被變更。
JavaScript 物件和陣列在預設情況下都是可變的。如果我建立一個物件,我可以變更其欄位的內容。如果我建立一個陣列,我也能變更其內容
const obj = { a: 1, b: 2 }
// still the same object outside, but the contents have changed
obj.b = 3
const arr = ['a', 'b']
// In the same way, we can change the contents of this array
arr.push('c')
arr[1] = 'd'
這稱為變異物件或陣列。它是在記憶體中相同的物件或陣列參考,但現在物件內的內容已變更。
為了不可變地更新值,程式碼必須建立現有物件/陣列的副本,然後修改副本.
我們可以使用 JavaScript 的陣列/物件展開運算子手動執行此操作,以及會傳回陣列新副本(而不是變異原始陣列)的陣列方法
const obj = {
a: {
// To safely update obj.a.c, we have to copy each piece
c: 3
},
b: 2
}
const obj2 = {
// copy obj
...obj,
// overwrite a
a: {
// copy obj.a
...obj.a,
// overwrite c
c: 42
}
}
const arr = ['a', 'b']
// Create a new copy of arr, with "c" appended to the end
const arr2 = arr.concat('c')
// or, we can make a copy of the original array:
const arr3 = arr.slice()
// and mutate the copy:
arr3.push('c')
Redux 預期所有狀態更新都以不可變的方式完成。稍後我們將探討這點為何重要以及如何做到,以及撰寫不可變更新邏輯的一些較簡單方法。
如需瞭解更多關於不可變性在 JavaScript 中如何運作的資訊,請參閱
術語
在繼續之前,你需要熟悉一些重要的 Redux 術語
動作
動作是一個具有 type
欄位的純 JavaScript 物件。你可以將動作視為描述應用程式中發生某事的事件。
type
欄位應為一個字串,用於為此動作提供描述性名稱,例如 "todos/todoAdded"
。我們通常會將該類型字串寫成 "domain/eventName"
,其中第一部分是此動作所屬的功能或類別,第二部分是發生具體事件。
動作物件可以有其他欄位,其中包含有關發生事件的其他資訊。根據慣例,我們將這些資訊放入稱為 payload
的欄位中。
典型的動作物件可能如下所示
const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}
動作建立器
動作建立器是一個建立並傳回動作物件的函式。我們通常使用這些建立器,以便我們不必每次都手動撰寫動作物件
const addTodo = text => {
return {
type: 'todos/todoAdded',
payload: text
}
}
簡化器
簡化器是一個接收當前 state
和 action
物件的函式,決定如何更新 state
(如有必要),並傳回新的 state
:(state, action) => newState
。你可以將簡化器視為一個事件監聽器,它根據接收的動作(事件)類型來處理事件。
「簡化器」函式之所以得名,是因為它們類似於你傳遞給 Array.reduce()
方法的回呼函式類型。
簡化器必須始終遵循一些特定規則
- 它們應僅根據
state
和action
參數計算新的狀態值 - 它們不被允許修改現有的
state
。相反,它們必須通過複製現有的state
並對複製的值進行更改來進行不可變更新。 - 它們不得執行任何非同步邏輯、計算隨機值或導致其他「副作用」
我們稍後會進一步探討 reducer 的規則,包括它們為何如此重要以及如何正確遵循這些規則。
reducer 函式中的邏輯通常遵循相同的步驟序列
- 檢查 reducer 是否關注這個動作
- 如果是,複製狀態,使用新值更新副本,然後傳回
- 否則,傳回現有狀態,不變更
以下是 reducer 的一個小範例,顯示每個 reducer 應遵循的步驟
const initialState = { value: 0 }
function counterReducer(state = initialState, action) {
// Check to see if the reducer cares about this action
if (action.type === 'counter/increment') {
// If so, make a copy of `state`
return {
...state,
// and update the copy with the new value
value: state.value + 1
}
}
// otherwise return the existing state unchanged
return state
}
reducer 可以使用任何類型的內部邏輯來決定新的狀態應為何:if/else
、switch
、迴圈等等。
詳細說明:為何它們稱為「reducer」?
Array.reduce()
方法讓你可以取得一個值陣列,一次處理陣列中的每個項目,並傳回單一最終結果。你可以將其視為「將陣列簡化為一個值」。
Array.reduce()
將回呼函式作為引數,它會對陣列中的每個項目呼叫一次。它採用兩個引數
previousResult
,你的回呼函式上次傳回的值currentItem
,陣列中的目前項目
回呼函式第一次執行時,沒有可用的 previousResult
,因此我們還需要傳入一個初始值,它將用作第一個 previousResult
。
如果我們想要將一個數字陣列加總,找出總和,我們可以撰寫一個看起來像這樣的 reduce 回呼函式
const numbers = [2, 5, 8]
const addNumbers = (previousResult, currentItem) => {
console.log({ previousResult, currentItem })
return previousResult + currentItem
}
const initialValue = 0
const total = numbers.reduce(addNumbers, initialValue)
// {previousResult: 0, currentItem: 2}
// {previousResult: 2, currentItem: 5}
// {previousResult: 7, currentItem: 8}
console.log(total)
// 15
請注意,這個 addNumbers
「reduce 回呼」函式不需要自己追蹤任何東西。它採用 previousResult
和 currentItem
引數,對它們執行一些操作,然後傳回一個新的結果值。
Redux reducer 函式與這個「reduce 回呼」函式的概念完全相同!它採用一個「先前結果」(state
)和「目前項目」(action
物件),根據這些引數決定一個新的狀態值,然後傳回那個新的狀態。
如果我們要建立一個 Redux 動作陣列,呼叫 reduce()
,然後傳入一個 reducer 函式,我們會以相同的方式取得最終結果
const actions = [
{ type: 'counter/increment' },
{ type: 'counter/increment' },
{ type: 'counter/increment' }
]
const initialState = { value: 0 }
const finalResult = actions.reduce(counterReducer, initialState)
console.log(finalResult)
// {value: 3}
我們可以說,Redux reducer 會將一組動作(隨著時間推移)簡化為一個單一狀態。不同之處在於,Array.reduce()
一次全部發生,而 Redux 則在執行中應用程式的生命週期中發生。
儲存
目前的 Redux 應用程式狀態存在於一個稱為儲存的物件中。
儲存是透過傳入一個 reducer 建立的,並且有一個稱為 getState
的方法,它傳回目前的狀態值
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counterReducer })
console.log(store.getState())
// {value: 0}
傳送
Redux 儲存有一個稱為 dispatch
的方法。更新狀態的唯一方法是呼叫 store.dispatch()
並傳入一個動作物件。儲存會執行其 reducer 函式,並將新的狀態值儲存在內部,我們可以呼叫 getState()
來擷取更新的值
store.dispatch({ type: 'counter/increment' })
console.log(store.getState())
// {value: 1}
你可以將傳送動作視為在應用程式中「觸發一個事件」。發生了一些事,我們希望儲存知道這件事。reducer 會像事件監聽器一樣運作,當它們聽到它們感興趣的動作時,它們會更新狀態以作為回應。
我們通常會呼叫動作建立器來傳送正確的動作
const increment = () => {
return {
type: 'counter/increment'
}
}
store.dispatch(increment())
console.log(store.getState())
// {value: 2}
選擇器
選擇器是函數,知道如何從儲存狀態值中提取特定資訊片段。隨著應用程式變大,這有助於避免重複邏輯,因為應用程式的不同部分需要讀取相同的資料
const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2
Redux 應用程式資料流程
稍早,我們討論了「單向資料流程」,它描述了更新應用程式的步驟順序
- 狀態描述應用程式在特定時間點的條件
- 使用者介面根據該狀態進行呈現
- 當某件事發生(例如使用者按一下按鈕)時,狀態會根據發生的事情進行更新
- 使用者介面根據新的狀態重新呈現
特別針對 Redux,我們可以將這些步驟分解得更詳細
- 初始設定
- 使用根部簡化器函數建立 Redux 儲存
- 儲存呼叫根部簡化器一次,並將回傳值儲存為其初始
狀態
- 當 UI 第一次呈現時,UI 元件會存取 Redux 儲存的目前狀態,並使用該資料決定要呈現什麼。它們也會訂閱任何未來的儲存更新,以便知道狀態是否已變更。
- 更新
- 應用程式中發生某些事,例如使用者按一下按鈕
- 應用程式程式碼會將動作傳送至 Redux 儲存,例如
dispatch({type: 'counter/increment'})
- 儲存再次執行簡化器函數,使用先前的
狀態
和目前的動作
,並將回傳值儲存為新的狀態
- 儲存會通知已訂閱的 UI 所有部分,表示儲存已更新
- 每個需要從儲存取得資料的 UI 元件都會檢查它們所需的狀態部分是否已變更。
- 每個看到其資料已變更的元件都會強制重新呈現新的資料,以便更新畫面中顯示的內容
以下是該資料流程的視覺化
您已學到的內容
Redux 確實有許多新的術語和概念需要記住。作為提醒,以下是我們剛剛介紹的內容
- Redux 是一個用於管理全域應用程式狀態的函式庫
- Redux 通常與 React-Redux 函式庫一起使用,以將 Redux 和 React 整合在一起
- Redux Toolkit 是撰寫 Redux 邏輯的建議方式
- Redux 使用「單向資料流」應用程式結構
- 狀態描述應用程式在某個時間點的條件,而 UI 則根據該狀態進行渲染
- 當應用程式中發生某事時
- UI 會傳送一個動作
- 儲存庫會執行還原器,而狀態會根據所發生的事件進行更新
- 儲存庫會通知 UI 狀態已變更
- 使用者介面根據新的狀態重新呈現
- Redux 使用多種類型的程式碼
- 動作是具有
type
欄位的純物件,且描述應用程式中「發生了什麼事」 - 還原器是根據先前狀態 + 動作計算新狀態值的函式
- Redux 儲存庫會在每次傳送 動作 時執行根還原器
- 動作是具有
下一步?
我們已經看過 Redux 應用程式的每個個別部分。接下來,繼續前往 第 2 部分:Redux Toolkit 應用程式結構,我們將查看一個完整的實際範例,以了解這些部分如何組合在一起。