2.基础教程-应用结构

创建 Redux Store

Redux Slice

“slice” 是应用中单个功能的 Redux reducer 逻辑和 action 的集合, 通常一起定义在一个文件中。

比如,在一个博客应用中,store 的配置大致长这样:

1
2
3
4
5
6
7
8
9
10
11
12
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,
},
})

创建 Slice Reducer 和 Action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { createSlice } from "@reduxjs/toolkit"

export const counterSlice = createSlice({
name: "counter",
initialState: {
value: 0,
},
reducers: {
increment: state => {
// Redux Toolkit 允许我们在 reducers 写 "可变" 逻辑。
// 并不是真正的改变 state 因为它使用了 immer 库
// 当 immer 检测到 "draft state" 改变时,会基于这些改变去创建一个新的不可变的 state
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

Redux Toolkit 有一个名为 createSlice 的函数,它负责生成 action 类型字符串、action creator 函数和 action 对象。

createSlice 内部使用了一个名为 Immer 的库。 Immer 使用一种 “Proxy” 包装你提供的数据,当你尝试 ”mutate“ 这些数据的时候,Immer 会跟踪你尝试进行的所有更改,然后使用该更改列表返回一个安全的、不可变的更新值,就好像你手动编写了所有不可变的更新逻辑一样。

Reducer 的规则

  • 仅使用 stateaction 参数计算新的状态值
  • 禁止直接修改 state。必须通过复制现有的 state 并对复制的值进行更改的方式来做 _不可变更新(immutable updates)_。
  • 禁止任何异步逻辑、依赖随机值或导致其他“副作用”的代码

“不可变更新(Immutable Updates)” 这个规则尤其重要,值得进一步讨论。

Reducer 与 Immutable 更新

在 Redux 中,**永远 不允许在 reducer 中直接更改 state 的原始对象!**

1
2
// ❌ 非法 - 默认情况下,这将更改 state!
state.value = 123

这就是为什么 Redux Toolkit 的 createSlice 函数可以让你以更简单的方式编写不可变更新!

警告

只能 在 Redux Toolkit 的 createSlicecreateReducer 中编写 “mutation” 逻辑,因为它们在内部使用 Immer!如果你在没有 Immer 的 reducer 中编写 mutation 逻辑,它 改变状态并导致错误!

用 Thunk 编写异步逻辑

到目前为止,我们应用程序中的所有逻辑都是同步的:

  1. dispatch action
  2. store 调用 reducer 来计算新状态
  3. dispatch 函数完成并结束

但是,我们的应用程序通常具有异步逻辑,我们需要一个地方在我们的 Redux 应用程序中放置异步逻辑。

thunk 是一种特定类型的 Redux 函数,可以包含异步逻辑。Thunk 是使用两个函数编写的:

  • 一个内部 thunk 函数,它以 dispatchgetState 作为参数
  • 外部创建者函数,它创建并返回 thunk 函数

示例:

1
2
3
4
5
6
7
8
9
// 外部的 thunk creator 函数, 它使我们可以执行异步逻辑
export const incrementAsync = amount => {
// 内部的 thunk 函数
return (dispatch, getState) => {
setTimeout(() => {
dispatch(incrementByAmount(amount)) // 调用dispatch修改store
}, 1000)
}
}

显然,incrementAsync() 返回的不是 action(action 是具有type字段的纯函数),而是一个函数,但它的使用方式和普通的 action 是一样的:

1
2
3
4
5
6
7
8
export function Counter() {
const dispatch = useAppDispatch();

return <>
<button onClick=()=>dispatch(increment()) >+</button>
<button onClick=()=>dispatch(incrementAsync()) >+</button>
</>
}

这是依赖 “middleware” 机制实现的,Redux 的 store 可以使用 “middleware” 进行扩展,中间件是一种可以添加额外功能的附加组件或插件。其最常见的用途就是实现异步逻辑,同时仍能与 store 对话。

Redux Thunk 中间件,代码很短:

1
2
3
4
5
6
7
8
9
10
const thunkMiddleware =
({ dispatch, getState }) =>
next =>
action => {
if (typeof action === "function") {
return action(dispatch, getState) // 这里面允许调用dispatch修改store
}

return next(action)
}

它先判断传入 dispatch 的 action 是函数还是对象。如果是一个函数,则调用函数,并返回结果。否则,传入的是普通 action 对象,就把这个 action 传递给 store 处理。

React Counter 组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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>
{/* 这里省略了额外的 render 代码 */}
</div>
)
}

使用 useSelector 提取数据

useSelector 这个 hook 让我们的组件从 Redux 的 store 状态树中提取它需要的任何数据。

我们默认 组件中不能引入 store。所以useSelector负责在幕后与 Redux store 对话。

示例:

1
const countPlusTwo = useSelector(state => state.counter.value + 2)

useSelector 会调用 store.getState() 获取 state,然后返回 state.counter.value + 2 的值。

每当一个 action 被 dispatch 并且 Redux store 被更新时,useSelector 将重新运行我们的选择器函数。如果选择器返回的值与上次不同,useSelector 将确保我们的组件使用新值重新渲染。

使用 useDispatch 来 dispatch action

类似地,我们知道如果我们可以访问 Redux store,可以 store.dispatch(increment())

由于我们无法访问 store 本身,因此我们需要某种方式来访问 dispatch 方法。

useDispatch hook 为我们完成了这项工作,并从 Redux store 中为我们提供了实际的 dispatch 方法:

1
const dispatch = useDispatch()

组件 State 与表单

在 React + Redux 应用中,你的全局状态应该放在 Redux store 中,你的本地状态应该保留在 React 组件中。

大多数表单的 state 不应该保存在 Redux 中。 相反,在编辑表单的时候把数据存到表单组件中,当用户提交表单的时候再 dispatch action 来更新 store。

Providing the Store

我们已经看到我们的组件可以使用 useSelectoruseDispatch 这两个 hook 与 Redux 的 store 通信。奇怪的是,我们并没有导入 store,那么这些 hooks 怎么知道要与哪个 Redux store 对话呢?

答案是使用 Context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from "react"
import { createRoot } from "react-dom/client"
import { Provider } from "react-redux"
import { store } from "app/store"
import App from "./App"

const container = document.getElementById("root")!
const root = createRoot(container)

root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
)

总结

  • 我们可以使用 Redux Toolkit configureStore API 创建一个 Redux store

    • configureStore 接收 reducer 函数来作为命名参数
    • configureStore 自动使用默认值来配置 store
  • 在 slice 文件中编写 Redux 逻辑

    • 一个 slice 包含一个特定功能或部分的 state 相关的 reducer 逻辑和 action
    • Redux Toolkit 的 createSlice API 为你提供的每个 reducer 函数生成 action creator 和 action 类型
  • Redux reducer 必须遵循以下原则

    • 必须依赖 stateaction 参数去计算出一个新 state
    • 如果要修改 state,只能先拷贝 state 副本,然后去修改副本
    • 不能包含任何异步逻辑或其他副作用
    • Redux Toolkit 的 createSlice API 内部使用了 Immer 库才达到表面上直接修改(”mutating”)state 也实现不可变更新(_immutable updates_)的效果
  • 一般使用 “thunks” 来开发特定的异步逻辑

    • Thunks 接收 dispatchgetState 作为参数
    • Redux Toolkit 内置并默认启用了 redux-thunk 中间件
  • 使用 React-Redux 来做 React 组件和 Redux store 的通信

    • 在应用程序根组件包裹 <Provider store={store}> 使得所有组件都能访问到 store
    • 全局状态应该维护在 Redux store 内,局部状态应该维护在局部 React 组件内