2.基础教程-应用结构

本文最后更新于:2023年12月5日 晚上

创建 Redux Store

Redux Slice

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

比如,在一个博客应用中,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,
 },
})

创建 Slice Reducer 和 Action

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 的原始对象!**

// ❌ 非法 - 默认情况下,这将更改 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 函数

示例:

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

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

export function Counter() {
   const dispatch = useAppDispatch();

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

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

Redux Thunk 中间件,代码很短:

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 组件

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 对话。

示例:

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 方法:

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

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 组件内

2.基础教程-应用结构
http://blog.lujinkai.cn/前端/React/redux/2.基础教程-应用结构/
作者
像方便面一样的男子
发布于
2023年5月29日
更新于
2023年12月5日
许可协议