
用一个计数器案例,深入搞懂 redux 原理

在本文中,我会用我觉得最适宜上手的学习顺序组织这篇入门 redux 的笔记。
- 在开头我会先介绍不依赖于框架的 redux 写法,这使得仅需要 JS 基础的小伙伴也能上手;
- 之后,我会详细总结现代 redux 的实现,通过这个能看懂现代的大多数使用 redux 的项目;
- 文末,我还会给出最新的 redux 特性,也就是 RTK v2 的一些区别和思考,以此紧跟社区最新进展和方向。
经典 Redux
Redux 并不一定要和 React 绑定,其本身就可以独立存在或者适配其他的框架。
我个人认为学习经典 Redux 实现是学习 Redux 的必经之路,所以下面就来拆解一个简单案例吧。
完整代码(下面会拆分解释):
1 | import {createStore} from 'redux'; |
1. store 的创建和订阅
counterReducer 函数
1 | function counterReducer(state = initialState, action) { |
counterReducer 就像一本操作手册,它内部定义了一系列操作,根据它接受的操作指令类型(也就是 action type),用字符串匹配这种平平无奇的方式找到对应的操作,然后执行它。
这其中有三个点需要多加关注:
1. 初始值
如果没人给它 state,它就用 initialState 开始工作 (state = initialState) 。
在这之后有了前一个 state,那么传入的就变成了 prevState,之后的 action 操作就会针对前一个值进行。
如此一来,再结合 action 机制,就可以进行"时间旅行“之类的操作来追踪 state 的变化。
2. state 的不可变性
我们一定不能直接修改 state 的值,
而是通过创建其副本然后再覆盖更新的方式,来修改它:
1 | return { |
我觉得几个重要的原因如下:
-
为了实现响应式更新
这是因为后续 store 需要比较prevState !== nextState
才知道状态变了,如果直接修改其值,那它的引用地址没变,store 无法检测到它的更新,后续更是没法更新 UI 了。 -
提供可预测性
每一次更新,我们都是创建出来一个新东西,旧状态不会丢失,如此你就可以知道变了哪里。之后,你甚至还可以记录、回滚、时间旅行。
在 React 中,总是应该避免修改原数据,避免副作用,这在很多地方都有体现。
3. fallback 策略中必须原封不动返回传入的 state
1 | default: |
这是在所有 action type 都匹配不上的时候做的最后的兜底工作。
为什么要返回 state ,而不是 null 或者直接不管它变成 undefined ?
这还是因为 store 会比较最后 RootState 有没有发生变化,你返回传入的 state,那就证明没匹配到操作,那自然也不需要发生变化,很合理。
如果返回 undefined ,会报错。如果返回 null,state 变成了 null,这就是发生了变化,但是实际上并没有进行操作,这不合理。
createStore 函数
1 | const store = createStore(counterReducer); |
这一步就是把”操作手册“注入 store,生成了专属于你的 store 实例。
到这里,store 内部的 currentState 就是 { counter: 0 }
subscribe 函数
1 | store.subscribe(() => { |
只要 state 被更新,subscribe 内部的回调函数就会执行。
这个内部的回调函数,相当于下文中会讲的 useSelector
函数中的 seletor
函数。
2. 使用 dispatch 修改 state
1 | store.dispatch( |
我们往 dispatch 函数里传入了一个 action。所谓 action ,其实就是一个对象,这个对象简单定义了一个用于操作 state 的指令。
[!question] 为什么必须通过 dispatch(action),不能直接修改 store.state?
这保证了 单向数据流 (One-Way Data Flow)。所有的 state 变化来源都是可追溯的(都是由某个 action 引起的),这让应用的状态变得可预测。
如果任何地方都可以随意修改 state,那么调试将成为一场噩梦。dispatch 就是那个唯一的、可控的入口。
3. 跳过安检,直达 Reducers
在这个简单的例子中,被 dispatch 后,我们没有经过中间件处理,action 指令会直奔 Reducers 。这就像一个没有安检的快速通道,在下文中会讲到 React + Redux 实现中,这个过程会经过复杂的处理。
4. 执行 Reducers
dispatch 之后,store 直接调用了上文中的 counterReducer 函数:
1 | function counterReducer(state = initialState, action) { |
这个函数接受两个参数:
- previousState: 当前 store 里的状态,也就是
{ counter: 0 }
。 -
- action: 刚刚 dispatch 的 action 对象
{type: 'amountAdded', payload: 5}
- action: 刚刚 dispatch 的 action 对象
之后,经过 switch 的匹配,进入了 amountAdded
那个 case ,内部真正的计算逻辑这时候才开始执行。
最后,Reducer 返回一个全新的对象:{ counter: 5 }。
5. Store 更新与订阅者通知
先保存
store 接收到 Reducer 返回的全新对象 { counter: 5 },并用它替换掉了内部旧的 { counter: 0 }。现在,store.getState() 的返回值就是 { counter: 5 } 了
再通知
store 立即查看它的订阅者列表,发现了我们之前用 store.subscribe() 注册的那个回调函数。于是,它执行了这个函数:
1 | () => { |
所以,控制台打印出了:state changed, now state : { counter: 5 }。
最后,你的主线程代码继续执行 console.log(store.getState());,它当然也打印出 { counter: 5 }。
这就是经典案例在 Redux 内部走过的完整旅程。清晰地展示了 Action -> Reducer -> New State -> Subscription 的核心循环。
React + Redux 的现代实现
在这个章节中,我使用现代化的 RTK方式,redux 风格采用社区推荐的 Ducks 模式。
[!note]- 关于 Ducks: Redux Reducer Bundles
很多老教程组织 redux 的方式和 ducks 大相径庭,关于这件事可参考以下资源:
一、逻辑层 (counterSlice.js)
我创建这样的文件:
src/features/counter/counterSlice.ts
:
1 | import {createAsyncThunk, createSlice, type PayloadAction} from "@reduxjs/toolkit"; |
关于 slice
1 | const counterSlice = createSlice({ |
这个 createSlice
很强大,具体的功能看看上面的代码注释应该能懂。
我还是想重点解释一下什么是 Slice。理解了这个词,就相当于理解了现代是怎么组织 redux 的。
Slice 是切片的意思,我从两个角度理解这个词:
1. 切片虽小,但是也是整体的快照。所以它包含完整的功能
createSlice
集成了 action 的命名、创建、state 更新、Reducers 定义等诸多功能。
现代开发讲求低耦合、高内聚。再没什么设计比这个更内聚的了。
2. 切片是整体的一部分
在过去,所有的逻辑都集中在一起,十分的混乱。
1 | // 一个臃肿的、未拆分的 reducer |
成百上千的 case,中间还有很多重复的命名前缀。如果两名开发者同时修改这个 switch ,还会导致 git 冲突。
我们使用 slice 切片,不同功能放在单独的文件里,各自负责各自的事情,管理一下子变得容易了。
关于导出语句
我们来看看这个文件都导出了什么东西:
导出 action creator
1 | export const {incremented, amountAdded} = counterSlice.actions; |
在搞清楚这个的作用之前,我一直有一个疑问:
在定义 amountAdded 方法时,有这样的代码:
1 | amountAdded(state, action: PayloadAction<number>) { |
但是之后用的时候,却是这样:
1 | dispatch(amountAdded(3)) |
-
明明 amountAdded 定义有两个参数 state 和 action,但用的时候只传了一个参数 3 ?
-
明明 amountAdded 函数内部的效果是操作 state 值,为什么在 dispatch 里的效果却变成创建了一个 action ?
这就要牵扯到现代 redux 的一个不同了。
你在 createSlice 中定义的 amountAdded 不是最终的 amountAdded。它实际上智能的生成了两个东西,一个是 action creator,另一个才是你真正写的 reducer 逻辑。完整的逻辑可能如下:
1 | // 第一步:定义 |
这也是为什么上面可以用 counterSlice.actions
的原因,其实就是从里面解构出需要的 actions creator。
导出 reducers
1 | export default counterSlice.reducer; |
这是为将来的 combineReducers 准备的。还记得上面的说的 slice 里的 [[#2. 切片是整体的一部分|第二点:切片是整体的一部分]] 吗?我们把这个切片导出去,将来可以用来构成那个所谓的整体。
也就是无数个 slice 里的 reducers ,构成了最后那个很多个 case 的 switch 。
二、 Store 配置 (store.js)
接着,我创建了这样的文件:
src/app/store.ts
:
1 | import {configureStore} from "@reduxjs/toolkit"; |
使用 RTK 的 configureStore,默认集成了很多好东西。它不仅仅是 createStore 的一个简单别名。它是一个高级的、带有“默认最佳实践”的工厂函数。
它有下面几个核心的功能:
1. 智能合并 Reducer
如果 reducer 是一个函数:
configureStore 会直接将其作为 root reducer。这和你使用 createStore(rootReducer) 的行为一致。
如果 reducer 是一个对象
这是 configureStore 的一个便捷特性。如果你传入一个像下面这样的对象:
1 | // 这个对象里面包含了一个个像 counterReducer 那样包含 switch 的函数 |
configureStore 会自动为你调用 combineReducers,将这个对象转换成一个 Root Reducer
。这省去了你手动导入和调用 combineReducers 的步骤。
最终,store 只需要比较这个 Root State 有没有变化即可决定要不要广播、更新 UI。
2. 自动配置 Redux DevTools Extension
这使得你可以直接使用 redux 浏览器插件,在这之前你需要写一堆恶心的样板代码来做。
3. 强大的默认 Middleware
它自动配置好了一组经过精心挑选、被社区广泛认为是“必备”的中间件,其中比较有用的像:
- redux-thunk:将来用于处理异步逻辑,简直必备。
- Immutability Check Middleware:用来检查你有没有违反 immutate 原则直接修改 state
- Serializability Check Middleware:检查你放入 action 和 state 中的所有值是否是“可序列化的”(即可以被 JSON.stringify 的),这和 支持持久化和水合 (Hydration) 有关。
4. 整合 createStore
完成了上述所有牛逼操作以后,最后才是进行了之前的 createStore 操作。
三、 typescript 支持(hooks.ts)
这里我先做一些有关于 typescript 的事情。我定义了两个 hook,这在之后组件中会用到,而不是使用原生的 useSelector 等。
src/app/hooks.ts
:
1 | // 导入必要的类型和原始 hooks |
创建类型增强的 selector hook
关于什么是 selector hook,可见 React 响应式原理
1 | // We create a new hook named `useAppSelector` |
-
发生了什么?
我们创建了一个新的常量(也是一个 Hook)叫做 useAppSelector。 -
它的类型是?
TypedUseSelectorHook<RootState>
。这是一个由 react-redux 提供的特殊类型,它的作用就是“创建一个预设了 state 类型的 useSelector”。我们把我们的 RootState 类型作为参数传给了它。 -
它的值是?
原始的 useSelector 函数。 -
结果是?
从今以后,在你的任何组件里,当你使用useAppSelector
时,它回调函数中的 state 参数会被自动推断为 RootState 类型。你再也不用手动写 (state: RootState) => … 了,VSCode 的自动补全也会变得无比智能!
如果不做这些…
如果直接在组件使用 useSelector,会有这样的问题:
- useSelector 的 state 是 unknown 类型,TypeScript 会报错,因为它不知道 state 的结构,你必须每次都手动为 state 添加类型:
1 | const count = useSelector((state: RootState) => state.counter.value); |
- 你的 IDE 不会有任何智能的提示,因为它不知道你的 Root State 树结构。
创建类型增强的 dispatch hook
1 | // We create a new hook named `useAppDispatch` |
-
发生了什么? 我们创建了一个新的函数(Hook)叫做 useAppDispatch。
-
它的内部实现? 它仅仅是调用了原始的 useDispatch,但关键在于,它在调用时,已经把 AppDispatch 这个类型参数
<AppDispatch>
“焊”上去了。 -
结果是? 从今以后,在你的任何组件里,只要你调用 useAppDispatch(),拿到的 dispatch 函数就已经被 TypeScript 完全理解了。它知道这个 dispatch 不仅能派发普通 action,还能派发你在 createSlice 中定义的异步 thunks。你再也不用在组件里写
<AppDispatch>
了
如果不做这些…
dispatch 也是一个泛型,它不知道你有哪些 thunk action。当你 dispatch 一个 thunk 时,类型推断可能会失效,所以你每次都需要手动指定类型:
1 | const dispatch = useDispatch<AppDispatch>(); |
四、UI 层(App.tsx)
首先要把 store 引进来,全是样板代码,主要就是需要用 <Provider>
包裹:
src/main.tsx
:
1 | import { StrictMode } from "react" |
在 React 组件中,我们使用刚才定义的 useAppSelector 和 useAppDispatch Hooks 来与 Redux 交互。
1 | // 导入刚才定义的 hooks |
1. 使用 useAppSelector 创建 UI 用到的动态数据
请见之前我写的文章: React 响应式原理,这里不多赘述
2. 使用 dispatch 发送更新数据指令
[!note]- dispatch 这个词的含义?
dispatch 源自拉丁语 dis-(分开)+ pactare(安排、协商),意思是“安排开去、分派> 出去”。一个贴合 dispatch 函数的例句:
The general dispatched troops to the front line.
将军把部队派往前线
dispatch 是 Redux store 实例上的一个核心方法 (store.dispatch)。
[!note]- store 实例上还有哪些方法?
扒了一下 redux 的源码:
1
2
3
4
5
6
7
8
9
10
11
12 interface Store<S = any, A extends Action = UnknownAction, StateExt extends > unknown = unknown> {
dispatch: Dispatch`<A>`;
getState(): S `&` StateExt;
subscribe(listener: ListenerCallback): Unsubscribe;
replaceReducer(nextReducer: Reducer<S, A>): void;
[Symbol.observable](): Observable<S & StateExt>;
}
你可以把它想象成一个信使派遣中心”。它是 唯一能够触发 state 变更的合法途径。你不能直接修改 store.getState() 返回的 state 对象,所有变更都必须通过派遣 action 来启动。
先创建实例:
1 | const dispatch = useAppDispatch(); |
在组件中使用:
1 | // 按钮点击逻辑。点这个按钮,最终会触发对应的 reducers 来执行数据更新 |
amountAdded(3)
实际上是一个传了 3 为 payload 的 action creator 函数,最终创建出这样的 action 给 dispatch 用:
1 | { |
那么,dispatch 在收到 action 后,到底会发生什么?
在之前经典的 redux 实现中,action 会直接被送给 Reducers 用。但是现在不一样了:
1 | ┌─────────────┐ dispatch(action) ┌─────────────────────┐ |
Middleware 链式处理
在 action 被真正送到 Reducer 手上之前,它会先经过一个叫做 Middleware (中间件) 的特殊关卡。可以把 Middleware 想象成 dispatch 和 reducer 之间的“管道”。Action 在这个管道里流动,每个 Middleware 都有机会观察、修改、延迟、甚至阻止这个 action。
在管道里,有一个很重要的检查操作:
1 | // redux-thunk 中间件: |
这是因为有时候,你 dispatch 的可能不是 action 对象,而是一个 Thunk Action Creator 函数,这个函数和异步操作有关,在这种情况下,中间件需要进行一些特殊处理,之后我会详细说明。
普通 action 对象情况
-
中间件会进行一次 action 合法性校验,大体就是检查你 action type 是不是字符串之类的,以及防止并发修改等等提升鲁棒性的操作。
-
在这之后,就可以传给 rootReducers 做真正的处理了:
1 | // 3. 所有中间件处理完后,到达核心 dispatch |
其中的第2️⃣步的 RootReducers 执行逻辑大概是这样:
- Redux 会调用每个 slice 的 reducer,内含 switch 逻辑
- 只有 counterReducer 匹配到了 action type,于是执行真正的 state 操作
- 到第 3️⃣ 步,完成 store 中的 state 替换
之后的响应式 UI 更新的细节,可见 React 响应式原理
异步情况
如果需要异步的更新 state,那又怎么办?
你可能会想:
直接在 counterSlice 的 Reducers 部分里加一个函数,内部执行异步操作不就行了吗?
但是实际上, Redux 要求 Reducer 必须是纯函数 —— 给定相同的 state 和 action,永远返回相同的 newState,并且不能有任何副作用(比如 API 请求)。
[!note]- 为什么我们需要纯函数?
React 官方 docs 专门针对这个问题有大篇幅说明,请见:
保持组件纯粹 – React 中文文档
下面,我们就一步步更改之前的程序,来实现一个异步的增加功能。
为了不依赖真实的后端,我们在 counterSlice.ts 文件中创建一个模拟函数:
1 | // src/features/counter/counterSlice.ts |
用 createAsyncThunk 创建 thunk action creator
在 counterSlice.ts 中,紧接着 initialState 定义之后,添加以下代码:
1 | // 创建一个异步 thunk,这个 incrementAsync 是一个函数,用于创建 action ,也就是 thunk action creator |
- 第一个参数的用途
incrementAsync
作为一个 action creator ,会根据你传入的第一个参数(也就是 counter/fetchCount
自动创建三个 action:
1 | // createAsyncThunk 会自动生成 3 个 action types: |
- 第二个参数的用途
第二个参数是一个称作 payloadCreator
的异步函数,这是我们真正的异步逻辑所在。
payloadCreator
异步的获取 action.payload,以便之后给 reducers 使用。
[!question]- 明明 payload creator 是一个 async 函数,为什么返回的东西最后能直接变成 action 的 payload?
async 无论如何都会返回一个 promise ,redux 内部对其进行解包过程。
[!question] 这个异步函数做什么?为什么叫 payload creator?
我们想想看执行异步操作的目的是什么:我们花时间等待一个结果,这个结果的值我们接下来会用到,实际上,就是异步的等待 action 对象里的 payload 值敲定,在确定了 payload 值以后,我们才可以真实的操作 state。
实际上,虽然刚才的案例只是展示了一个参数,但是 payloadCreator 其实有两个参数 (arg, thunkAPI)
。
- arg: 为本次异步操作提供独特的、必要的输入数据,传递给真正的异步函数用。每次 dispatch 都可以是不同的值。如果某个 thunk 不需要输入,可以忽略它(用 _)。
- thunkAPI:可以理解为一个工具箱,其中有几件有用的工具:
- 工具1:getState:
假设当前 slice 有一个异步操作: API 请求。请求需要附带一个认证 Token,这个 Token 又存在另一个 slice 里: auth slice 里。
这时候,我们就需要用这个工具获得信息:
1 | async (arg, thunkAPI) => { |
- 工具2:dispatch:
想象在开始一个耗时很长的异步任务时,我们想先弹出一个通知。这时候,需要 dispatch 别的 slice 里的 reducer :
1 | import { showNotification } from '../notifications/notificationSlice'; |
- 工具3:rejectWithValue (非常重要!)
用于当任务可预见地失败时,用一种标准化的方式中止任务,并附带清晰的失败报告。
想象用户提交的表单数据验证失败,或者 API 返回了明确的错误信息:
1 | async (formData, thunkAPI) => { |
thunkAPI.rejectWithValue('网络连接失败,请检查网络后重试')
括号里的东西最后会被用作 action 里的 payload 值。
相比直接throw new Error(),我们把错误信息放在 payload 里,非常干净,可以直接用于 UI 显示错误提示。
修改一下 state 的结构
1 | // 为了处理异步加载状态,我们需要在 state 中添加一个 status 字段 |
其实就是多加了一个 status 字段,用于标记状态。这在以后 UI 中可以用到,来显示加载状态之类的。
在 extraReducers 里添加方法
前文说过,不能直接在 Reducers 部分添加异步方法,所以我们需要额外添加一个 extraReducers:
1 | const counterSlice = createSlice({ |
builder 的核心就是它有一个 addCase 方法,这个方法有两个参数。
在解释第一个参数之前,先明白 JavaScript 的一个特性 :
- 函数本身也是对象,可以给函数添加属性
所以我们可以看到 incrementAsync.pending
这种东西,incrementAsync 是一个 action creator ,所以它实际上对应着就是 pending 状态的 action,如此一来 action type 就敲定了。
所以,incrementAsync.pending
实际上等效于 'counter/fetchCount/pending'
。
事实上,addCase 本身会对传入的第一个参数做一个校验,如果直接传了字符串,比如 'counter/fetchCount/pending'
,那就会直接使用,此时和 switch 里的字符串 action type 没什么区别。
如果发现传入的是 action creator(incrementAsync.fulfilled
也是一个 action creator ),那就调用 action creator 的 type 属性。没错,每一个 action creator 都有 type 属性,用于直接读取它要创建的 action 的 type。
这不只是存在于 thunk action creator,普通的也一样:
1 | console.log(amountAdded.type) // 会输出:"counter/amountAdded" |
.addCase 的第二个参数就是我们需要修改的 state,这个和同步的部分没什么区别。
在 addCase 内部,我们开始写真实的操作 state 的代码,就和之前同步的部分一样。
在组件中使用
其他的一样,还是用 dispatch :
1 | dispatch(incrementAsync(5)) |
dispatch 之后,中间件检测到这是一个函数,而不是简单的 action 对象。于是,会触发以下流程:
- 第一步:立即 dispatch 一个 pending 状态的 action
1 | // thunk 内部自动 dispatch pending action |
你应该注意到了异步的 dispatch 结构和同步的不一样。
在里面并没有 payload,这是因为 pending 状态下 payload 还没有得到,需要在前文提到的 payload creator 执行之后才有。
meta 里的 requestID 是 createAsyncThunk 根据时间戳和随机数自动生成的,真实情况是 36 位,这主要为了追踪和调试。
其中的 arg 前文提过了,是给异步函数的参数。
- 第二步:执行 Pending Action 通过 Middleware 和 Reducer
我们在前文中定义过:
1 | .addCase(incrementAsync.pending, state => { |
这个是 Reducer,所以刚才发出的 Pending Action 直接通过中间件来找他了。
- 第三步:异步操作执行
也就是执行 payloadCreator 函数,这个函数是 createAsyncThunk 里用到的第二个参数。
执行成功后,我们就有 payload 的真实值了。
- 第四步:dispatch 第二个 action
这个 action 有真实的 payload ,之后的流程就和同步的部分没两样了:
1 | // 500ms 后,thunk 继续执行,dispatch fulfilled action |
dispatch 之后,同样经过中间件,会被最终送到之前定义的这个 reducer 执行:
1 | .addCase(incrementAsync.fulfilled, (state, action) => { |
之后 UI 更新部分就和之前一样了,详细可见 React 响应式原理。
RTK v2
上文所有部分都是 RTK v1 的实现,RTK v2 在部分情况下会显得更加简洁、直观。
下面是一些新的特性和示例代码,对之前的例子进行升级。
自定义我们的 slice
简单来说,之前的 Slice 的 Reducers 部分不能放异步函数,需要 extraReducers 这种不优雅的玩意。但是现在可以,不过需要我们做一件事:
自定义一个有异步能力的 slice:
新建文件 src/app/createAppSlice.ts
:
1 | // src/app/createAppSlice.ts |
这个 buildCreateSlice 函数也是从 RTK 里导过来的,RTK 不再提供一个功能固化的 createSlice,而是提供了一个 “createSlice 的工厂”,这个工厂就是 buildCreateSlice。
buildCreateSlice 的核心配置项就是 creators。
这有点像插件的感觉,asyncThunkCreator 就是其中一个插件,开发者当然也可以写更多自己的“插件”,来扩展 slice 的能力。
这种模式下,Redux 更加开放,充分体现了为什么很多人说 React 可以应用于大型项目。
自定义 slice 的 Reducers 特性
1 | // src/features/counter/counterSlice.ts |
重点关注 Reducers 部分即可。
有两个不同:
- 之前的 reducers 就是一个对象,现在是一个有 create 作为参数的函数:
- 对于常规 reducer,直接使用
create.reducer()
- 对于异步 reducer,使用
create.asyncThunk(payloadCreator, reducers)
- 对于常规 reducer,直接使用
虽然看起来只是多了一层 create.reducer() 的包裹,但这样做是有好处的。这个包裹允许 RTK 在未来添加更多元数据或增强功能,同时保持了 API 的一致性。它向阅读代码的人明确表示:“这是一个标准的、同步的 reducer”。
- 之前 incrementAsync 是单独用 createAsyncThunk 创建的,现在直接写在了 reducers 里,很舒服。
自定义 slice 的 Selector 特性
我们还能在上面看到这样的代码:
1 | // 5. 定义 Selectors |
1 | // 7. 导出 Selectors |
之后在组件中用的时候,可以直接这样写:
1 | const count = useAppSelector(selectCount) |
之前是这样:
1 | // 之前 |
没什么可以多说的,一看就知道代码更加内聚了。
更直观易懂的 hooks 类型配置
src/app/hooks.ts
:
1 | // 新版 hooks.ts |
这是因为 v2 版本的 RTK , useSelector 和 useDispatch 都有 withTypes 方法了,确实比之前看起来更直观易懂。
更支持 RTK Query 的 store 配置
src/app/store.ts
:
1 | import type { Action, ThunkAction } from "@reduxjs/toolkit" |
这部分比之前的麻烦很多,但是对 RTK Query 有更好的支持。
TODO:补充新版支持 RTK Query 的 store 解读。
- 标题: 用一个计数器案例,深入搞懂 redux 原理
- 作者: 三葉Leaves
- 创建于 : 2025-08-07 00:00:00
- 更新于 : 2025-08-15 12:08:24
- 链接: https://blog.oksanye.com/6233daf4fc5d/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。