Redux Essentials,第 2 部分:Redux Toolkit 應用程式結構
- 典型的 React + Redux Toolkit 應用程式的結構
- 如何在 Redux DevTools Extension 中查看狀態變更
簡介
在 第 1 部分:Redux 概觀與概念 中,我們探討了 Redux 為何有用、用於描述 Redux 程式碼不同部分的術語和概念,以及資料如何透過 Redux 應用程式流動。
現在,讓我們來看一個實際運作的範例,了解這些部分如何結合在一起。
反例應用程式
我們將檢視的範例專案是一個小型計數器應用程式,它讓我們在按鈕時增加或減少數字。它可能不是非常令人興奮,但它顯示了 React+Redux 應用程式中所有重要部分的實際運作。
這個專案使用 Create-React-App 的官方 Redux 範本 建立。它已經預先設定好標準的 Redux 應用程式結構,使用 Redux Toolkit 建立 Redux 儲存體和邏輯,以及 React-Redux 將 Redux 儲存體和 React 元件連接在一起。
以下是專案的線上版本。您可以透過按一下右側應用程式預覽中的按鈕來玩玩看,並瀏覽左側的原始檔。
如果您想在自己的電腦上嘗試建立這個專案,您可以使用我們的 Redux 範本 開始一個新的 Create-React-App 專案
npx create-react-app redux-essentials-example --template redux
使用計數器應用程式
計數器應用程式已經設定好讓我們在使用時觀察其內部發生的事情。
開啟瀏覽器的 DevTools。然後,選擇 DevTools 中的「Redux」標籤,並按一下右上角工具列中的「State」按鈕。您應該會看到類似這樣的畫面
在右側,我們可以看到我們的 Redux 儲存體一開始的應用程式狀態值如下所示
{
counter: {
value: 0
}
}
DevTools 會在我們使用應用程式時顯示儲存體狀態如何改變。
讓我們先玩玩應用程式,看看它做了什麼。按一下應用程式中的「+」按鈕,然後查看 Redux DevTools 中的「Diff」標籤
我們可以在這裡看到兩件重要的事情
- 當我們按一下「+」按鈕時,一個類型為
"counter/increment"
的動作會傳送到儲存體 - 當該動作傳送時,
state.counter.value
欄位從0
變為1
現在嘗試以下步驟
- 再次按一下「+」按鈕。顯示的值現在應該是 2。
- 按一下「-」按鈕一次。顯示的值現在應該是 1。
- 按一下「新增金額」按鈕。顯示的值現在應該是 3。
- 將文字方塊中的數字「2」變更為「3」
- 按一下「非同步新增」按鈕。您應該會看到一個進度條填滿按鈕,幾秒鐘後,顯示的值會變更為 6。
回到 Redux DevTools。您應該會看到總共發送了五個動作,每次按一下按鈕就會發送一個。現在從左側的清單中選取最後一個 「counter/incrementByAmount」
項目,然後按一下右側的「動作」標籤
我們可以看到這個動作物件看起來像這樣
{
type: 'counter/incrementByAmount',
payload: 3
}
如果您按一下「差異」標籤,您會看到 state.counter.value
欄位從 3
變更為 6
來回應那個動作。
能夠看到應用程式內部發生了什麼事,以及我們的狀態如何隨著時間而變更,是非常強大的功能!
DevTools 有更多命令和選項,可以協助您除錯應用程式。試試按一下右上方的「追蹤」標籤。您應該會在面板中看到一個 JavaScript 函式堆疊追蹤,其中有數個原始碼區段,顯示動作到達儲存時執行的程式碼列。特別會有一個程式碼列被突顯:我們從 <Counter>
元件發送這個動作的程式碼列
這讓追蹤程式碼的哪個部分發送特定動作變得更簡單。
應用程式內容
現在您知道應用程式做了什麼,讓我們看看它是如何運作的。
以下是組成這個應用程式的關鍵檔案
/src
index.js
:應用程式的起點App.js
:頂層 React 元件/app
store.js
:建立 Redux 儲存執行個體
/features
/counter
Counter.js
:顯示計數器功能使用者介面的 React 元件counterSlice.js
:計數器功能的 Redux 邏輯
讓我們從了解 Redux 儲存是如何建立的開始。
建立 Redux 儲存
開啟 app/store.js
,它應該如下所示
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'
export default configureStore({
reducer: {
counter: counterReducer
}
})
Redux store 是使用 Redux Toolkit 的 configureStore
函數建立的。configureStore
要求我們傳入 reducer
參數。
我們的應用程式可能由許多不同的功能組成,而每個功能可能都有自己的 reducer 函數。當我們呼叫 configureStore
時,我們可以在物件中傳入所有不同的 reducer。物件中的金鑰名稱將定義我們最終狀態值中的金鑰。
我們有一個名為 features/counter/counterSlice.js
的檔案,它匯出一個用於計數邏輯的 reducer 函數。我們可以在這裡匯入那個 counterReducer
函數,並在建立 store 時包含它。
當我們傳入一個像 {counter: counterReducer}
的物件時,這表示我們希望在我們的 Redux 狀態物件中有一個 state.counter
區段,並且我們希望 counterReducer
函數負責決定是否以及如何更新 state.counter
區段,無論何時派送動作。
Redux 允許使用不同類型的外掛程式(「middleware」和「enhancers」)自訂 store 設定。configureStore
預設會自動將幾個 middleware 加入 store 設定中,以提供良好的開發人員體驗,並設定 store,以便 Redux DevTools Extension 可以檢查其內容。
Redux 切片
「切片」是 Redux reducer 邏輯和應用程式中單一功能的動作集合,通常在單一檔案中一起定義。這個名稱來自將根 Redux 狀態物件分割成多個狀態「切片」。
例如,在部落格應用程式中,我們的 store 設定可能如下所示
import { configureStore } from '@reduxjs/toolkit'
import usersReducer from '../features/users/usersSlice'
import postsReducer from '../features/posts/postsSlice'
import commentsReducer from '../features/comments/commentsSlice'
export default configureStore({
reducer: {
users: usersReducer,
posts: postsReducer,
comments: commentsReducer
}
})
在那個範例中,state.users
、state.posts
和 state.comments
都是 Redux 狀態的個別「切片」。由於 usersReducer
負責更新 state.users
切片,我們稱它為「切片 reducer」函數。
詳細說明:Reducer 和狀態結構
建立 Redux store 時,需要傳入單一的「根 reducer」函數。因此,如果我們有許多不同的切片 reducer 函數,我們如何取得單一的根 reducer,以及這如何定義 Redux store 狀態的內容?
如果我們嘗試手動呼叫所有切片還原器,它看起來可能像這樣
function rootReducer(state = {}, action) {
return {
users: usersReducer(state.users, action),
posts: postsReducer(state.posts, action),
comments: commentsReducer(state.comments, action)
}
}
它個別呼叫每個切片還原器,傳入 Redux 狀態的特定切片,並將每個回傳值包含在最終新的 Redux 狀態物件中。
Redux 有個稱為 combineReducers
的函式,會自動為我們執行這項工作。它接受一個包含切片還原器的物件作為其引數,並回傳一個函式,該函式會在每次傳送動作時呼叫每個切片還原器。每個切片還原器的結果都會合併在一起,成為最終結果的單一物件。我們可以使用 combineReducers
執行與前一個範例相同的工作
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer,
comments: commentsReducer
})
當我們將切片還原器的物件傳遞給 configureStore
時,它會將這些物件傳遞給 combineReducers
,以便我們產生根還原器。
正如我們先前所見,您也可以直接將還原器函式傳遞為 reducer
引數
const store = configureStore({
reducer: rootReducer
})
建立切片還原器和動作
由於我們知道 counterReducer
函式來自 features/counter/counterSlice.js
,讓我們逐一檢視該檔案中的內容。
import { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: state => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
},
decrement: state => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
}
}
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
稍早,我們看到點選 UI 中的不同按鈕會傳送三個不同的 Redux 動作類型
{type: "counter/increment"}
{type: "counter/decrement"}
{type: "counter/incrementByAmount"}
我們知道動作是具有 type
欄位的純粹物件,type
欄位永遠是字串,而且我們通常有建立並回傳動作物件的「動作建立器」函式。那麼,這些動作物件、類型字串和動作建立器是在哪裡定義的?
我們可以每次都手動撰寫這些內容。但是,那樣會很乏味。此外,Redux 中真正重要的是還原器函式,以及它們計算新狀態的邏輯。
Redux Toolkit 有個稱為 createSlice
的函式,它會負責產生動作類型字串、動作建立器函式和動作物件。您只需要為這個切片定義一個名稱,撰寫一個包含一些還原器函式的物件,它就會自動產生對應的動作程式碼。name
選項的字串用作每個動作類型的第一部分,每個還原器函式的金鑰名稱用作第二部分。因此,"counter"
名稱 + "increment"
還原器函式產生了 {type: "counter/increment"}
的動作類型。(畢竟,如果電腦可以為我們執行這項工作,為什麼還要手動撰寫呢!)
除了 name
欄位,createSlice
需要我們傳入 reducer 的初始狀態值,以便在第一次呼叫時有 state
。在此範例中,我們提供一個物件,其中 value
欄位一開始為 0。
我們可以在此看到有三個 reducer 函式,這對應於按不同按鈕所觸發的三個不同動作類型。
createSlice
會自動產生動作建立器,其名稱與我們撰寫的 reducer 函式相同。我們可以呼叫其中一個來查看它回傳的內容,藉此確認這點
console.log(counterSlice.actions.increment())
// {type: "counter/increment"}
它也會產生區塊 reducer 函式,該函式知道如何回應所有這些動作類型
const newState = counterSlice.reducer(
{ value: 10 },
counterSlice.actions.increment()
)
console.log(newState)
// {value: 11}
Reducer 的規則
我們之前提到 reducer 務必遵循一些特殊規則
- 它們只能根據
state
和action
參數計算新的狀態值 - 它們不能修改現有的
state
。相反地,它們必須透過複製現有的state
並變更複製的值來進行不可變更新。 - 它們不能執行任何非同步邏輯或其他「副作用」
但為什麼這些規則很重要?有幾個不同的原因
- Redux 的目標之一是讓您的程式碼可預測。當函式的輸出只根據輸入參數計算時,就比較容易了解該程式碼如何運作,並進行測試。
- 另一方面,如果函式依賴於函式外部的變數,或隨機運作,您永遠不知道執行時會發生什麼事。
- 如果函式修改其他值,包括其參數,這可能會以意外的方式變更應用程式的運作方式。這可能是常見的錯誤來源,例如「我更新了我的狀態,但現在我的 UI 沒有在它應該更新時更新!」
- Redux DevTools 的一些功能仰賴您的 reducer 正確遵循這些規則
關於「不可變更新」的規則特別重要,值得進一步討論。
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 使用者最常犯的錯誤。
這就是為什麼 Redux Toolkit 的 createSlice
函式讓您可以更輕鬆地編寫不可變更新!
createSlice
在內部使用一個名為 Immer 的函式庫。Immer 使用一個稱為 Proxy
的特殊 JS 工具來包裝您提供的資料,並讓您可以編寫「變異」該包裝資料的程式碼。但是,Immer 會追蹤您嘗試進行的所有變更,然後使用該變更清單回傳一個安全且不可變的更新值,就像您手動編寫所有不可變更新邏輯一樣。
因此,代替這個
function handwrittenReducer(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}
您可以編寫看起來像這樣的程式碼
function reducerWithImmer(state, action) {
state.first.second[action.someId].fourth = action.someValue
}
這樣更容易閱讀!
但是,這裡有一些非常重要的注意事項
您只能在 Redux Toolkit 的 createSlice
和 createReducer
中編寫「變異」邏輯,因為它們在內部使用 Immer!如果您在沒有 Immer 的 reducer 中編寫變異邏輯,它將變異狀態並導致錯誤!
牢記這一點,讓我們回頭看看計數器區塊中的實際 reducer。
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: state => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
},
decrement: state => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
}
}
})
我們可以看到 increment
reducer 會永遠將 1 加到 state.value
。因為 Immer 知道我們已經對 draft state
物件進行了變更,所以我們不必實際回傳任何東西。同樣地,decrement
reducer 會減去 1。
在這些 reducer 中,我們實際上不需要讓我們的程式碼檢視 action
物件。它會無論如何傳遞,但由於我們不需要它,所以我們可以跳過宣告 action
作為 reducer 的參數。
另一方面,incrementByAmount
reducer 確實 需要知道一件事:它應該將多少加到計數器值。因此,我們宣告 reducer 具有 state
和 action
參數。在這種情況下,我們知道輸入到文字方塊中的數量會放入 action.payload
欄位,所以我們可以將它加到 state.value
。
有關不可變性和撰寫不可變更新的更多資訊,請參閱 「不可變更新模式」文件頁面 和 React 和 Redux 中不可變性的完整指南。
使用 thunk 撰寫非同步邏輯
到目前為止,我們應用程式中的所有邏輯都是同步的。動作會被派送,儲存體會執行 reducer 並計算新的狀態,而派送函數會結束。但是,JavaScript 語言有許多方法可以撰寫非同步的程式碼,而我們的應用程式通常會有非同步邏輯,例如從 API 中擷取資料。我們需要一個地方來放置 Redux 應用程式中的非同步邏輯。
thunk 是一種特定的 Redux 函數,可以包含非同步邏輯。thunk 是使用兩個函數撰寫的
- 一個內部 thunk 函數,它取得
dispatch
和getState
作為參數 - 一個外部建立函數,它建立並回傳 thunk 函數
從 counterSlice
匯出的下一個函數是 thunk 動作建立函數的一個範例
// The function below is called a thunk and allows us to perform async logic.
// It can be dispatched like a regular action: `dispatch(incrementAsync(10))`.
// This will call the thunk with the `dispatch` function as the first argument.
// Async code can then be executed and other actions can be dispatched
export const incrementAsync = amount => dispatch => {
setTimeout(() => {
dispatch(incrementByAmount(amount))
}, 1000)
}
我們可以像使用典型的 Redux 動作建立函數一樣使用它們
store.dispatch(incrementAsync(5))
但是,使用 thunk 需要在建立 Redux 儲存體時將 redux-thunk
middleware(Redux 的一種外掛程式類型)加入 Redux 儲存體。幸運的是,Redux Toolkit 的 configureStore
函數已經自動為我們設定好,所以我們可以繼續使用 thunk。
當您需要進行 AJAX 呼叫從伺服器擷取資料時,您可以將該呼叫放入 thunk 中。以下是一個寫得比較長的範例,讓您可以看到它是如何定義的
// the outside "thunk creator" function
const fetchUserById = userId => {
// the inside "thunk function"
return async (dispatch, getState) => {
try {
// make an async call in the thunk
const user = await userAPI.fetchById(userId)
// dispatch an action when we get the response back
dispatch(userLoaded(user))
} catch (err) {
// If something went wrong, handle it here
}
}
}
我們將在 第 5 部分:非同步邏輯和資料擷取 中看到 thunk 的使用
詳細說明:Thunk 和非同步邏輯
我們知道不可以在 reducer 中放置任何非同步邏輯。但是,該邏輯必須存在於某個地方。
如果我們可以存取 Redux store,我們可以撰寫一些非同步程式碼,並在完成時呼叫 store.dispatch()
const store = configureStore({ reducer: counterReducer })
setTimeout(() => {
store.dispatch(increment())
}, 250)
但是,在實際的 Redux 應用程式中,我們不允許將 store 匯入其他檔案,特別是在我們的 React 元件中,因為這會讓該程式碼更難以測試和重複使用。
此外,我們通常需要撰寫一些非同步邏輯,我們知道最終會與某些 store 一起使用,但我們不知道哪個 store。
Redux store 可以使用「middleware」進行擴充,這是一種附加元件或外掛程式,可以新增額外的功能。使用 middleware 最常見的原因是讓你可以撰寫可以包含非同步邏輯的程式碼,但同時仍可以與 store 對話。它們也可以修改 store,以便我們可以呼叫 dispatch()
,並傳入不是純粹動作物件的值,例如函式或 Promise。
Redux Thunk middleware 修改 store,讓你可以在 dispatch
中傳遞函式。事實上,它夠短,我們可以將它貼在這裡
const thunkMiddleware =
({ dispatch, getState }) =>
next =>
action => {
if (typeof action === 'function') {
return action(dispatch, getState)
}
return next(action)
}
它會查看傳遞到 dispatch
中的「動作」實際上是函式,而不是純粹的動作物件。如果它實際上是函式,它會呼叫該函式,並傳回結果。否則,由於這一定是動作物件,它會將動作傳遞到 store。
這讓我們可以撰寫任何我們想要的同步或非同步程式碼,同時仍可以存取 dispatch
和 getState
。
這個檔案中還有另一個函式,但我們稍後在查看 <Counter>
UI 元件時會討論它。
請參閱 Redux Thunk 文件、文章 Thunk 到底是什麼? 和 Redux 常見問答條目「為什麼我們使用 middleware 進行非同步?」 以取得更多資訊。
React 計數器元件
稍早,我們看到了獨立的 React <Counter>
元件的樣子。我們的 React+Redux 應用程式有一個類似的 <Counter>
元件,但它做了一些不同的處理。
我們將從查看 Counter.js
元件檔案開始
import React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {
decrement,
increment,
incrementByAmount,
incrementAsync,
selectCount
} from './counterSlice'
import styles from './Counter.module.css'
export function Counter() {
const count = useSelector(selectCount)
const dispatch = useDispatch()
const [incrementAmount, setIncrementAmount] = useState('2')
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
<span className={styles.value}>{count}</span>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
-
</button>
</div>
{/* omit additional rendering output here */}
</div>
)
}
與早先的純粹 React 範例一樣,我們有一個稱為 Counter
的函式元件,它會將一些資料儲存在 useState
勾子中。
然而,在我們的元件中,它看起來不像我們將實際的目前計數器值儲存在狀態中。有一個稱為 count
的變數,但它不是來自 useState
勾子。
React 雖然包含了幾個內建的勾子,例如 useState
和 useEffect
,但其他函式庫也可以建立自己的 自訂勾子,使用 React 的勾子來建構自訂邏輯。
React-Redux 函式庫 有一個 自訂勾子組,讓你的 React 元件可以與 Redux 儲存體互動。
使用 useSelector
讀取資料
首先,useSelector
勾子讓我們的元件可以從 Redux 儲存體狀態中擷取它需要的任何資料片段。
前面我們看到,我們可以撰寫「選擇器」函式,它將 state
作為引數,並傳回狀態值的一部分。
我們的 counterSlice.js
在底部有這個選擇器函式
// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state) => state.counter.value)`
export const selectCount = state => state.counter.value
如果我們可以存取 Redux 儲存體,我們可以這樣擷取目前的計數器值
const count = selectCount(store.getState())
console.log(count)
// 0
我們的元件無法直接與 Redux 儲存體通訊,因為我們無法將它匯入元件檔案。但是,useSelector
會在幕後處理與 Redux 儲存體的通訊。如果我們傳入一個選擇器函式,它會為我們呼叫 someSelector(store.getState())
,並傳回結果。
因此,我們可以透過執行以下動作來取得目前的儲存體計數器值
const count = useSelector(selectCount)
我們也不一定要只使用已經匯出的選擇器。例如,我們可以撰寫一個選擇器函式作為 useSelector
的內嵌引數
const countPlusTwo = useSelector(state => state.counter.value + 2)
每當動作被傳送,而 Redux 儲存體已更新,useSelector
會重新執行我們的選擇器函式。如果選擇器傳回的值與上次不同,useSelector
會確保我們的元件使用新值重新渲染。
使用 useDispatch
傳送動作
類似地,我們知道如果我們可以存取 Redux 儲存體,我們可以使用動作建立器來傳送動作,例如 store.dispatch(increment())
。由於我們無法存取儲存體本身,我們需要一些方法來存取 dispatch
方法。
useDispatch
勾子為我們執行這個動作,並提供我們 Redux 儲存體中的實際 dispatch
方法
const dispatch = useDispatch()
從這裡,我們可以在使用者執行某些動作時傳送動作,例如按一下按鈕
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
元件狀態和表單
現在你可能會想:「我是否一定要將所有應用程式的狀態都放入 Redux 儲存區中?」
答案是否。應用程式中需要在各處使用的全域狀態應放入 Redux 儲存區中。只在一個地方需要的狀態應保留在元件狀態中。
在此範例中,我們有一個輸入文字方塊,使用者可以在其中輸入要新增到計數器的下一個數字
const [incrementAmount, setIncrementAmount] = useState('2')
// later
return (
<div className={styles.row}>
<input
className={styles.textbox}
aria-label="Set increment amount"
value={incrementAmount}
onChange={e => setIncrementAmount(e.target.value)}
/>
<button
className={styles.button}
onClick={() => dispatch(incrementByAmount(Number(incrementAmount) || 0))}
>
Add Amount
</button>
<button
className={styles.asyncButton}
onClick={() => dispatch(incrementAsync(Number(incrementAmount) || 0))}
>
Add Async
</button>
</div>
)
我們可以透過在輸入的 onChange
處理常式中發送動作,並將目前的數字字串保留在我們的簡化器中,來將其保留在 Redux 儲存區中。但是,這對我們沒有任何好處。該文字字串唯一使用的地方是在此 <Counter>
元件中。(當然,在此範例中只有一個其他元件:<App>
。但即使我們有一個包含許多元件的較大型應用程式,也只有 <Counter>
會在意此輸入值。)
因此,將該值保留在 <Counter>
元件中的 useState
勾子中是有道理的。
類似地,如果我們有一個名為 isDropdownOpen
的布林旗標,應用程式中沒有其他元件會在意它 - 它應真正保留在此元件中。
在 React + Redux 應用程式中,你的全域狀態應放入 Redux 儲存區中,而你的區域狀態應保留在 React 元件中。
如果你不確定要將某個項目放在哪裡,以下是一些用於判斷應將哪種類型的資料放入 Redux 的常見經驗法則
- 應用程式的其他部分是否在意此資料?
- 你是否需要能夠根據此原始資料建立進一步的衍生資料?
- 是否使用相同的資料來驅動多個元件?
- 你是否重視能夠將此狀態還原到某個時間點(例如,時光旅行除錯)?
- 你是否想要快取資料(例如,如果資料已存在,則使用狀態中的資料,而不是重新要求它)?
- 你是否想要在熱重新載入 UI 元件(交換時可能會遺失其內部狀態)時保持此資料一致?
這也是一個關於如何思考 Redux 中表單的良好範例。大多數表單狀態可能不應保留在 Redux 中。相反地,在編輯表單元件時將資料保存在表單元件中,然後在使用者完成時發送 Redux 動作來更新儲存。
在我們繼續之前,還有一件事要注意:還記得 counterSlice.js
中的 incrementAsync
thunk 嗎?我們在此元件中使用它。請注意,我們使用它的方式與我們發送其他一般動作建立函數的方式相同。此元件不關心我們是否正在發送一般動作或啟動某些非同步邏輯。它只知道當您按一下該按鈕時,它會發送某個東西。
提供儲存
我們已經看到我們的元件可以使用 useSelector
和 useDispatch
鉤子與 Redux 儲存進行通訊。但是,由於我們沒有匯入儲存,這些鉤子如何知道要與哪個 Redux 儲存進行通訊?
現在我們已經看過此應用程式的所有不同部分,是時候回到此應用程式的起點,看看拼圖的最後幾塊如何拼湊在一起。
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import store from './app/store'
import { Provider } from 'react-redux'
import * as serviceWorker from './serviceWorker'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
我們必須始終呼叫 ReactDOM.render(<App />)
來告訴 React 開始呈現我們的根 <App>
元件。為了讓我們的鉤子(例如 useSelector
)正常運作,我們需要使用稱為 <Provider>
的元件來傳遞 Redux 儲存,以便它們可以存取它。
我們已經在 app/store.js
中建立我們的儲存,因此我們可以在這裡匯入它。然後,我們將 <Provider>
元件放在整個 <App>
的周圍,並傳入儲存:<Provider store={store}>
。
現在,任何呼叫 useSelector
或 useDispatch
的 React 元件都將與我們提供給 <Provider>
的 Redux 儲存進行通訊。
你學到了什麼
儘管計數器範例應用程式相當小,但它顯示了 React + Redux 應用程式的所有關鍵部分共同運作。以下是我們涵蓋的內容
- 我們可以使用 Redux Toolkit
configureStore
API 建立 Redux 儲存configureStore
接受reducer
函數作為命名參數configureStore
自動使用良好的預設設定設定儲存
- Redux 邏輯通常會整理到稱為「區塊」的檔案中
- 「區塊」包含與 Redux 狀態的特定功能/區段相關的 reducer 邏輯和動作
- Redux Toolkit 的
createSlice
API 會為你提供的每個個別 reducer 函式產生動作建立器和動作類型
- Redux reducer 必須遵循特定規則
- 應僅根據
state
和action
參數計算新的狀態值 - 必須透過複製現有狀態來進行不可變更新
- 不能包含任何非同步邏輯或其他「副作用」
- Redux Toolkit 的
createSlice
API 使用 Immer 來允許「變異」不可變更新
- 應僅根據
- 非同步邏輯通常會寫在稱為「thunk」的特殊函式中
- Thunk 會接收
dispatch
和getState
作為參數 - Redux Toolkit 預設會啟用
redux-thunk
中介軟體
- Thunk 會接收
- React-Redux 允許 React 元件與 Redux 儲存體互動
- 使用
<Provider store={store}>
包裝應用程式可讓所有元件使用儲存體 - 全域狀態應放入 Redux 儲存體中,而區域狀態應保留在 React 元件中
- 使用
下一步?
現在你已經看過 Redux 應用程式的所有部分,是時候撰寫你自己的程式了!在本教學課程的其餘部分中,你將建立一個使用 Redux 的大型範例應用程式。在此過程中,我們將介紹你使用 Redux 的正確方式所需了解的所有關鍵概念。
繼續前往 第 3 部分:基本的 Redux 資料流程 開始建立範例應用程式。