Redux Essentials,第二部分:Redux Toolkit 应用结构
- 典型的 React + Redux Toolkit 应用的结构
- 如何在 Redux DevTools 扩展中查看状态更改
简介
在 第 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”选项卡,并点击右上角工具栏中的“状态”按钮。您应该会看到类似于这样的内容
在右侧,我们可以看到我们的 Redux 存储从一个看起来像这样的应用程序状态值开始
{
counter: {
value: 0
}
}
DevTools 将向我们展示当我们使用应用程序时存储状态是如何变化的。
让我们先玩一下应用程序,看看它做了什么。点击应用程序中的“+”按钮,然后查看 Redux DevTools 中的“差异”选项卡
在这里我们可以看到两件重要的事情
- 当我们点击“+”按钮时,一个类型为
"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 组件,显示计数器功能的 UIcounterSlice.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
部分,无论何时调度一个 action。
Redux 允许使用不同类型的插件(“中间件”和“增强器”)来定制 store 设置。configureStore
默认情况下会自动将几个中间件添加到 store 设置中,以提供良好的开发体验,并且还会设置 store,以便 Redux DevTools Extension 可以检查其内容。
Redux 切片
“切片”是应用程序中单个功能的 Redux reducer 逻辑和 action 的集合,通常在一个文件中一起定义。这个名称来自将根 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 存储需要在创建时传入一个单一的“根 reducer”函数。因此,如果我们有许多不同的切片 reducer 函数,我们如何获得一个单一的根 reducer,以及这如何定义 Redux 存储状态的内容?
如果我们尝试手动调用所有切片 reducer,它可能看起来像这样
function rootReducer(state = {}, action) {
return {
users: usersReducer(state.users, action),
posts: postsReducer(state.posts, action),
comments: commentsReducer(state.comments, action)
}
}
这分别调用每个切片 reducer,传入 Redux 状态的特定切片,并将每个返回值包含在最终的新的 Redux 状态对象中。
Redux 有一个名为 combineReducers
的函数,它可以自动为我们完成此操作。它接受一个包含切片 reducer 的对象作为参数,并返回一个函数,该函数在每次调度操作时调用每个切片 reducer。来自每个切片 reducer 的结果都组合在一起形成一个单一对象作为最终结果。我们可以使用 combineReducers
做与之前示例相同的事情
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer,
comments: commentsReducer
})
当我们将切片 reducer 的对象传递给 configureStore
时,它会将这些对象传递给 combineReducers
以便为我们生成根 reducer。
正如我们之前看到的,您也可以直接将 reducer 函数作为 reducer
参数传递
const store = configureStore({
reducer: rootReducer
})
创建切片 Reducer 和操作
由于我们知道 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 中真正重要的是 reducer 函数,以及它们用于计算新状态的逻辑。
Redux Toolkit 提供了一个名为 createSlice
的函数,它负责生成动作类型字符串、动作创建器函数和动作对象。您只需为该切片定义一个名称,编写一个包含一些 reducer 函数的对象,它就会自动生成相应的动作代码。name
选项中的字符串用作每个动作类型的第一个部分,每个 reducer 函数的键名用作第二个部分。因此,"counter"
名称 + "increment"
reducer 函数生成了一个动作类型 {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 将始终将 state.value
加 1。由于 Immer 知道我们已经对草稿 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
中间件(一种 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.dispatch()
const store = configureStore({ reducer: counterReducer })
setTimeout(() => {
store.dispatch(increment())
}, 250)
但是,在真实的 Redux 应用程序中,我们不允许将存储导入到其他文件中,尤其是在我们的 React 组件中,因为这会使该代码更难测试和重用。
此外,我们经常需要编写一些异步逻辑,我们知道这些逻辑最终将与某个存储一起使用,但我们不知道是哪个存储。
Redux 存储可以通过“中间件”进行扩展,中间件是一种类似于附加组件或插件的东西,可以添加额外的功能。使用中间件最常见的原因是让你编写可以包含异步逻辑的代码,但同时仍然可以与存储进行通信。它们还可以修改存储,以便我们可以调用 `dispatch()` 并传入不是普通操作对象的的值,例如函数或 Promise。
Redux Thunk 中间件修改了存储,让你可以将函数传递给 `dispatch`。实际上,它足够短,我们可以在这里粘贴它。
const thunkMiddleware =
({ dispatch, getState }) =>
next =>
action => {
if (typeof action === 'function') {
return action(dispatch, getState)
}
return next(action)
}
它会查看传递给 `dispatch` 的“操作”实际上是一个函数,而不是一个普通操作对象。如果它实际上是一个函数,它会调用该函数,并返回结果。否则,由于这必须是一个操作对象,它会将操作转发给存储。
这给了我们一种方法来编写我们想要的任何同步或异步代码,同时仍然可以访问 `dispatch` 和 `getState`。
这个文件中还有一个函数,但我们将在稍后查看 `<Counter>` UI 组件时讨论它。
请参阅 Redux Thunk 文档、帖子 什么是 thunk? 以及 Redux 常见问题解答条目“为什么我们使用中间件进行异步操作?” 以获取更多信息。
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>
)
我们可以将当前数字字符串保存在 Redux 存储中,方法是在输入的 onChange
处理程序中调度一个操作并将其保存在我们的 reducer 中。但是,这不会给我们带来任何好处。该文本字符串仅在此处使用,即在 <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 storeconfigureStore
接受一个reducer
函数作为命名参数configureStore
自动使用良好的默认设置设置 store
- 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 store 交互
- 使用
<Provider store={store}>
包装应用程序使所有组件都可以使用 store - 全局状态应该放在 Redux store 中,局部状态应该保留在 React 组件中
- 使用
下一步
现在您已经看到了 Redux 应用程序中所有部分的实际操作,是时候编写您自己的应用程序了!在本教程的其余部分,您将构建一个使用 Redux 的更大的示例应用程序。在此过程中,我们将涵盖您以正确方式使用 Redux 所需的所有关键概念。
继续到 第 3 部分:基本 Redux 数据流 开始构建示例应用程序。