Redux Essentials 详解(二):React + Redux Toolkit 应用结构——Store、Slice 与状态管理规则
本篇基于 Redux 官方教程 "Redux Essentials Part 2: Redux Toolkit App Structure"(docs/tutorials/essentials/part-2-app-structure.md)展开,以一个可运行的计数器示例应用为线索,完整拆解一个典型 React + Redux Toolkit 应用的文件结构与代码组织方式:如何用 configureStore 创建 store、如何用 createSlice 组织每个功能片的 reducer 与 action、如何书写符合"不可变更新"规则的 reducer、如何用 selector 与 thunk 读取状态和执行异步逻辑,以及 React-Redux 如何通过 <Provider>、useSelector、useDispatch 把 store 接入组件树。读完本文,你将能够对照仓库内的 counter-ts 示例 逐文件理解 Redux 应用各部分的职责与相互关系。
计数器示例应用:整体结构与 DevTools 观察
教程使用的示例项目是一个小型计数器应用:点击按钮可以对一个数字进行加、减、按指定值累加、以及异步累加。项目由 Redux Toolkit 的 Vite 模板简化而来,开箱即用地配置了标准 Redux 应用结构:用 Redux Toolkit 创建 store 与逻辑,用 React-Redux 把 store 和 React 组件连接起来。
如果想在本地搭建这个示例,可以用以下命令创建副本(命令来自教程原文):
# 创建精简版示例项目
npx degit reduxjs/redux-templates/packages/rtk-app-structure-example my-app
# 或者使用完整的 Redux Toolkit Vite 模板
npx degit reduxjs/redux-templates/packages/vite-template-redux my-app
在 Redux DevTools 中观察状态变化
示例应用已经被设置为"边用边看内部变化"。打开浏览器 DevTools,切换到 "Redux" 标签页,点击工具栏右上角的 "State" 按钮,会看到 store 的初始状态:
初始应用状态形如:
{
counter: {
value: 0,
status: 'idle'
}
}
接下来按顺序操作应用,观察 DevTools 的变化:
- 点击 "+" 按钮,切换到 Redux DevTools 的 "Diff" 标签页:
此时可以确认两件关键事实:
- 点击 "+" 按钮时,一个
type为"counter/increment"的 action 被 dispatch 到 store; - 该 action 处理时,
state.counter.value字段从0变成了1。
-
继续操作:再点一次 "+"(值变为 2);点一次 "-"(值变为 1);点击 "Add Amount" 按钮(值变为 3);把输入框中的 "2" 改为 "3";点击 "Add Async" 按钮——会看到按钮上出现进度条,几秒后显示值变为 6。
-
回到 Redux DevTools,此时应该能看到一共 5 次 dispatch 的 action(每次点击对应一次)。选中最后一条
"counter/incrementByAmount",点击右侧 "Action" 标签页,可以看到该 action 对象形如:
{
type: 'counter/incrementByAmount',
payload: 3
}
切换 "Diff" 标签页,可以看到 state.counter.value 因为这个 action 从 3 变成了 6。
- 再点击右上角的 "Trace" 标签页,可以看到一段 JavaScript 函数调用栈,其中高亮的那一行正是
<Counter>组件中 dispatch 该 action 的代码位置,方便追踪"是哪个代码触发了这次状态变化":
能够实时看到应用内部发生了什么、状态如何随时间变化,正是 Redux DevTools 最强大的能力之一。
应用的关键文件清单
教程将示例应用的关键文件归纳如下(对照本仓库的 examples/counter-ts 目录,可以看到同一结构在 Create React App 版本中的落地):
/srcmain.tsx:应用入口(仓库示例中对应 index.tsx)App.tsx:顶层 React 组件/appstore.ts:创建 Redux store 实例hooks.ts:导出预定义类型的 React-Redux hooks
/features/counterCounter.tsx:展示 counter 功能 UI 的 React 组件counterSlice.ts:counter 功能的 Redux 逻辑
下面从 store 的创建开始,逐文件深入。
创建 Redux Store
app/store.ts 的内容(教程版本)如下:
import type { Action, ThunkAction } from '@reduxjs/toolkit'
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '@/features/counter/counterSlice'
export const store = configureStore({
reducer: {
counter: counterReducer
}
})
// Infer the type of `store`
export type AppStore = typeof store
export type RootState = ReturnType<AppStore['getState']>
// Infer the `AppDispatch` type from the store itself
export type AppDispatch = AppStore['dispatch']
// Define a reusable type describing thunk functions
export type AppThunk<ThunkReturnType = void> = ThunkAction<
ThunkReturnType,
RootState,
unknown,
Action
>
要点解析:
configureStore必须传入reducer参数。应用由多个功能(feature)组成时,每个功能通常有自己的 reducer;调用configureStore时把这些 reducer 放进一个对象,对象的键名就会成为最终 state 中对应的字段名。传入{counter: counterReducer}的含义是:Redux state 中要有state.counter这个区段,每当有 action 被 dispatch 时,由counterReducer决定state.counter是否更新以及如何更新。- default export 的导入命名是自由 ES Module 语法。
features/counter/counterSlice.ts以 ESM "default" 方式导出 reducer 函数,导入时可以起任意名字(这里叫counterReducer)。这一点与 Redux 无关,是标准 ES Module 的 import/export 行为。 configureStore自动完成一系列配置:默认加好若干中间件(如redux-thunk)以获得良好的开发体验,并自动接入 Redux DevTools Extension,让扩展可以检查 store 内容——这正是上面 DevTools 观察之所以"开箱即用"的原因。- TypeScript 类型导出:
RootState、AppDispatch、AppThunk等基于 store 推导出的可复用类型会在这里导出,供 slice 和组件文件使用。
源码佐证:store 创建与 dispatch 的核心约束
对照本仓库核心实现可以看出 configureStore 之下的机制。在 createStore.ts 中,基础 dispatch 会先校验 action 必须是普通对象:
// src/createStore.ts 中 dispatch 的校验逻辑
function dispatch(action: A) {
if (!isPlainObject(action)) {
throw new Error(
`Actions must be plain objects. Instead, the actual type was: ...`
)
}
...
}
也就是说,未加中间件的 store 只接受普通对象形式的 action——dispatch 函数、Promise 等非普通对象会直接抛错。这就从源码层面解释了为什么 dispatch 函数形式的 thunk 需要中间件支持(详见下文 Thunk 一节),也解释了教程中"configureStore 已自动配置好中间件,所以可以直接 dispatch thunk"这一说法。
再看本仓库中的真实示例 examples/counter-ts/src/app/store.ts:
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'
export const store = configureStore({
reducer: {
counter: counterReducer
}
})
export type AppDispatch = typeof store.dispatch
export type RootState = ReturnType<typeof store.getState>
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>
两个版本结构完全一致,差别只在类型推导写法(仓库示例直接用 typeof store.dispatch 推导 AppDispatch,并用相对路径导入 slice)和 AppThunk 的泛型参数(Action<string>)。这说明教程中的 store.ts 并非孤立代码,而是与仓库内示例同源的标准写法。
Redux Slice:把状态树切成"功能片"
"slice"(切片)是某个功能对应的 Redux reducer 逻辑与 action 的集合,通常集中定义在同一个文件里。名字来源于把根 state 对象拆分成多个"slice"。
比如一个博客应用的 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 const store = configureStore({
reducer: {
users: usersReducer,
posts: postsReducer,
comments: commentsReducer
}
})
这里 state.users、state.posts、state.comments 各自是 Redux state 的一个"slice"。由于 usersReducer 负责更新 state.users 区段,我们称它为 slice reducer 函数。
详细解释:从多个 slice reducer 到一个 root reducer
Redux store 创建时需要传入一个单一的 "root reducer"。那么多个 slice reducer 如何合并成一个 root reducer?如果手工调用,逻辑大致是:
function rootReducer(state = {}, action) {
return {
users: usersReducer(state.users, action),
posts: postsReducer(state.posts, action),
comments: commentsReducer(state.comments, action)
}
}
即用当前状态中对应区段调用每个 slice reducer,再把返回值合并进新的 state 对象。Redux 提供了 combineReducers 自动完成这件事:接受一个 slice reducer 对象,返回一个"每次 dispatch 时依次调用每个 slice reducer、并将结果合并为最终对象"的函数:
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer,
comments: commentsReducer
})
configureStore 收到 reducer 对象后会将其交给 combineReducers 生成 root reducer;当然你也可以直接传一个现成的 root reducer:
const store = configureStore({
reducer: rootReducer
})
源码佐证:combineReducers 的校验与短路机制
仓库中的 combineReducers.ts 实现印证了上述行为,还包含两条值得了解的机制:
- 初始化校验(见 src/combineReducers.ts#L62-L94):
assertReducerShape会先用INITaction 探测每个 slice reducer,若初始化时返回undefined直接抛错——因为 slice reducer 在state为undefined时必须返回初始状态。 - 变更短路优化(见 src/combineReducers.ts#L177-L199):
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
...
hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length
return hasChanged ? nextState : state
只有当某个 slice 的引用发生变化(或键数量变化)时才返回新对象,否则直接返回原 state。这是 store 层面引用相等性(=== 比较)的基础,也是 React-Redux 中"selector 返回值不变则组件不重渲染"能够成立的前提。另外,若 slice reducer 对某 action 返回 undefined,combineReducers 会抛出明确的错误(提示"必须显式返回 previous state 才能忽略 action"),帮助开发者尽早发现 reducer 规则违规。
创建 Slice Reducer 与 Action
既然 counterReducer 来自 features/counter/counterSlice.ts,逐段看该文件(教程版本):
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
// Define the TS type for the counter slice's state
export interface CounterState {
value: number
status: 'idle' | 'loading' | 'failed'
}
// Define the initial value for the slice state
const initialState: CounterState = {
value: 0,
status: 'idle'
}
// Slices contain Redux reducer logic for updating state, and
// generate actions that can be dispatched to trigger those updates.
export const counterSlice = createSlice({
name: 'counter',
initialState,
// The `reducers` field lets us define reducers and generate associated actions
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
},
// Use the PayloadAction type to declare the contents of `action.payload`
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload
}
}
})
// Export the generated action creators for use in components
export const { increment, decrement, incrementByAmount } = counterSlice.actions
// Export the slice reducer for use in the store configuration
export default counterSlice.reducer
之前在 DevTools 中看到三种 action type:{type: "counter/increment"}、{type: "counter/decrement"}、{type: "counter/incrementByAmount"}。action 是带 type 字段的普通对象,type 总是字符串,通常由 "action creator" 函数创建并返回。这些 action 对象、type 字符串和 creator 函数在哪里定义?
答案是:不必手写,createSlice 会全部自动生成。它的工作方式是:
- 提供
name(如"counter")作为每个 action type 的前缀; reducers对象中每个 reducer 函数的键名作为 action type 的后缀;"counter"+"increment"生成{type: "counter/increment"};- 同时生成与 reducer 同名的 action creator,以及认识所有这些 action type 的 slice reducer 函数。
验证方式(调用生成物查看返回):
console.log(counterSlice.actions.increment())
// {type: "counter/increment"}
const newState = counterSlice.reducer(
{ value: 10 },
counterSlice.actions.increment()
)
console.log(newState)
// {value: 11}
除了 name,createSlice 还需要初始 state(initialState),这样 reducer 第一次被调用时就有 state 可用:这里是一个 value: 0、status: 'idle' 的对象。reducers 中定义了三个 reducer 函数,恰好对应 UI 上三种按钮点击产生的三种 action type。
仓库示例中的同一份 slice
仓库中 examples/counter-ts/src/features/counter/counterSlice.ts 与教程片段基本一致(额外包含后文要讲的 incrementAsync、incrementIfOdd 与两个 selector),可以逐行对照阅读,例如:
// examples/counter-ts/src/features/counter/counterSlice.ts
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: state => {
state.value += 1
},
decrement: state => {
state.value -= 1
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload
}
},
...
})
Reducer 的规则
Redux 的 reducer 必须始终遵守三条规则:
- 只能基于
state和action两个参数计算新 state; - 不允许修改现有
state,必须做"不可变更新"(immutable updates):复制现有 state 后在副本上修改; - 必须是"纯函数"——不能包含任何异步逻辑或其他"副作用"。
为什么这些规则重要?
- Redux 的目标之一是让代码可预测:函数输出只由输入决定时,行为更容易理解、更容易测试;
- 如果函数依赖外部变量或随机行为,运行时结果不可预期;
- 如果函数修改了包括参数在内的外部值,会以不可预期的方式改变应用行为——这是常见 bug 来源,比如"我明明更新了 state,UI 却不在该更新时更新";
- 部分 Redux DevTools 能力(如 time-travel 调试)依赖于 reducer 正确遵循这些规则。
Reducer 与不可变更新
"mutation"(修改现有对象/数组值)与"immutability"(把值视为不可变)的区别,在 Redux 里有一条铁律:reducer 绝不允许修改原始/当前 state 值!
// ❌ 非法 - 默认情况下这会直接修改 state!
state.value = 123
必须在 Redux 中禁止 mutation 的原因:
- 会导致 bug,例如 UI 不能正确显示最新值;
- 让"state 为什么/如何被更新"更难理解;
- 让编写测试变得更困难;
- 破坏"时间旅行调试"的正确性;
- 违背 Redux 的设计意图与使用模式。
既然不能改原值,如何返回更新后的 state?Reducer 只能对原值做"复制",然后修改副本:
// ✅ 安全,因为我们做了副本
return {
...state,
value: 123
}
手工写不可变更新可以使用展开运算符(spread)等"返回副本"的手段(教程 Part 1 的 "Immutability" 一节有完整示例),但必须承认:手写很难记也容易出错——在 reducer 中不小心 mutation 是 Redux 使用者犯得最多的单一错误。
这就是 Redux Toolkit 的 createSlice 让不可变更新变得更容易的原因。 createSlice 内部使用 Immer 库:Immer 用 JS 的 Proxy 机制包装你提供的数据,让你写"修改"包装后数据的代码;Immer 会追踪你想做的所有修改,再基于这份修改记录返回一个"如同手写全部不可变逻辑"般安全更新的值。对比感受一下——手写的深度更新:
function handwrittenReducer(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}
用 Immer 风格只需要:
function reducerWithImmer(state, action) {
state.first.second[action.someId].fourth = action.someValue
}
后者显然易读得多。但有一条必须牢记的限制:
只有在 Redux Toolkit 的
createSlice和createReducer中才能写"mutating"风格逻辑,因为它们在内部使用了 Immer!如果脱离 Immer 在你的代码里写 mutating 逻辑,真的会修改 state 并引发 bug。
带着这一点回看 counter slice 的 reducer:
increment总是给state.value加 1。由于 Immer 知道我们对"草稿"state做了修改,这里不需要 return 任何值;decrement同理减 1。- 这两个 reducer 不需要读取
action对象。action仍会被传入,但用不到时可以省掉action形参。 incrementByAmount则必须知道"加多少":输入框中的数字被放进了action.payload,所以声明(state, action)两个形参,把action.payload加到state.value上。- TypeScript 下需要告诉编译器
action.payload的类型。PayloadAction<T>的含义是"这是一个 action 对象,且action.payload的类型为 T"。UI 已把文本框中的字符串数字转成 number 再 dispatch,因此声明为action: PayloadAction<number>。
更多不可变更新模式可参考仓库文档 Immutable Update Patterns。
附加 Redux 逻辑:Selector 与 Thunk
Redux 的核心是 reducer、action 和 store,但还有两类常见函数值得掌握。
用 Selector 读取数据
可以调用 store.getState() 拿到整个根 state,再访问 state.counter.value 等字段。标准做法是写 "selector" 函数替我们完成这些字段读取。counterSlice.ts 导出了两个可复用的 selector:
// Selector functions allows us to select a value from the Redux root state.
// Selectors can also be defined inline in the `useSelector` call
// in a component, or inside the `createSlice.selectors` field.
export const selectCount = (state: RootState) => state.counter.value
export const selectStatus = (state: RootState) => state.counter.status
(仓库示例 examples/counter-ts/src/features/counter/counterSlice.ts#L71 中的定义与此一致。)
- selector 以整个根 state 为参数调用,可以读出特定值,也可以做计算并返回新值;
- 使用 TypeScript 时,需要用
store.ts导出的RootState类型给 selector 的state参数标注类型; - 不必为每个 slice 的每个字段都单独建 selector。本示例这么做只是为了演示写法;实际项目中应在"写多少 selector"上把握平衡,更多背景见 Deriving Data with Selectors;
- selector 的深度用法(如结合 React-Redux 读取、
createSlice.selectors派生 selector)在教程 Part 4: Using Redux Data 展开,性能优化(memoize)在 Part 6: Performance 展开。
用 Thunk 编写异步逻辑
到目前为止,应用的逻辑都是同步的:action 被 dispatch,store 运行 reducer 计算新 state,dispatch 结束。但 JS 有大量异步写法,真实应用通常有"从 API 取数"等异步逻辑,这些逻辑在 Redux 应用中要有安放之处。
thunk 是一种可以包含异步逻辑的 Redux 函数,由两个函数构成:
- 内层 thunk 函数:接收
dispatch和getState作为参数; - 外层 creator 函数:创建并返回这个 thunk 函数。
counterSlice 中的手写 thunk 示例:
// The function below is called a thunk, which can contain both sync and async logic
// that has access to both `dispatch` and `getState`. They can be dispatched like
// a regular action: `dispatch(incrementIfOdd(10))`.
// Here's an example of conditionally dispatching actions based on current state.
export const incrementIfOdd = (amount: number): AppThunk => {
return (dispatch, getState) => {
const currentValue = selectCount(getState())
if (currentValue % 2 === 1) {
dispatch(incrementByAmount(amount))
}
}
}
仓库示例 examples/counter-ts/src/features/counter/counterSlice.ts#L75-L82 中的实现与之相同(箭头函数嵌套的等价写法)。在这个 thunk 中:getState() 取 store 当前根 state,dispatch() 再 dispatch 另一个 action。这里同样可以轻松放入 setTimeout、await 等异步逻辑。用法与普通 action creator 完全一致:
store.dispatch(incrementIfOdd(6))
注意前提:使用 thunk 要求 store 创建时加入了 redux-thunk 中间件(一种 Redux 插件)。configureStore 已自动完成这一步,所以可以直接用。从源码看,未经中间件包装的 store 在 src/createStore.ts#L270-L277 中对非普通对象的 dispatch 直接抛错(前文已展示),而中间件会重写 dispatch 链来拦截这类值——这正是 thunks "必须依赖中间件"的底层原因。
编写 thunk 时,dispatch 与 getState 的类型要正确。可以逐个标注 (dispatch: AppDispatch, getState: () => RootState),但标准做法是在 store 文件中定义可复用的 AppThunk 类型(store.ts 中已导出)。
需要向服务器发 HTTP 请求时,把调用放进 thunk。一个稍长、展示完整结构的手写示例:
// the outside "thunk creator" function
const fetchUserById = (userId: string): AppThunk => {
// the inside "thunk function"
return async (dispatch, getState) => {
try {
dispatch(userPending())
// 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
}
}
}
Redux Toolkit 还提供 createAsyncThunk:它替我们完成全部 dispatch 工作——dispatch 这个 thunk 时,发起请求前先 dispatch pending action,异步逻辑结束后 dispatch fulfilled 或 rejected action。counterSlice 中用 createAsyncThunk 发起了一个模拟 API 请求:
// Thunks are commonly used for async logic like fetching data.
// The `createAsyncThunk` method is used to generate thunks that
// dispatch pending/fulfilled/rejected actions based on a promise.
// In this example, we make a mock async request and return the result.
// The `createSlice.extraReducers` field can handle these actions
// and update the state with the results.
export const incrementAsync = createAsyncThunk(
'counter/fetchCount',
async (amount: number) => {
const response = await fetchCount(amount)
// The value we return becomes the `fulfilled` action payload
return response.data
}
)
其中 fetchCount 是仓库示例 examples/counter-ts/src/features/counter/counterAPI.ts 里的模拟异步请求(500ms 后 resolve {data: amount}),可以直观理解"pending → fulfilled"之间那几秒按钮上进度条的来源。
使用 createAsyncThunk 时,在 createSlice 的 extraReducers 中处理它生成的 action。这里处理全部三种,既更新 status 又更新 value:
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
// omit reducers
},
// The `extraReducers` field lets the slice handle actions defined elsewhere,
// including actions generated by createAsyncThunk or in other slices.
extraReducers: builder => {
builder
// Handle the action types defined by the `incrementAsync` thunk defined below.
// This lets the slice reducer update the state with request status and results.
.addCase(incrementAsync.pending, state => {
state.status = 'loading'
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = 'idle'
state.value += action.payload
})
.addCase(incrementAsync.rejected, state => {
state.status = 'failed'
})
}
})
详细解释:为什么异步逻辑要用 Thunk
reducer 中不允许任何异步逻辑,但这些逻辑总得有地方放。如果手里有 store 引用,完全可以写:
const store = configureStore({ reducer: counterReducer })
setTimeout(() => {
store.dispatch(increment())
}, 250)
但真实应用中不允许把 store 直接导入其他文件(尤其是 React 组件),那会让代码更难测试和复用;而且我们常常要写"最终会用在某个 store 上、但还不知道是哪个 store"的异步逻辑。
Redux store 可以通过"中间件"(middleware,一种插件)扩展。使用中间件最常见的理由,就是让你能写"带异步逻辑、又能与 store 对话"的代码;它还可以改造 store,使 dispatch() 能接收非普通 action 对象(如函数、Promise)。Redux Thunk 中间件修改 store 使其接受函数形式的 dispatch,代码短到可以直接贴出:
const thunkMiddleware =
({ dispatch, getState }) =>
next =>
action => {
if (typeof action === 'function') {
return action(dispatch, getState)
}
return next(action)
}
它检查传入 dispatch 的"action"是否其实是函数:是函数就调用它并返回结果;否则视为 action 对象,原样传给 store 后续链。这样,我们就可以在任意同步/异步代码中同时持有 dispatch 和 getState。
仓库核心包中同样提供了中间件组合的实现,见 src/applyMiddleware.ts(将多个中间件组合成 store enhancer);thunk 的详细用法会在教程 Part 5: Async Logic and Data Fetching 继续展开,背景讨论见 Writing Logic in Thunks 与 FAQ: Actions。
React Counter 组件
Counter.tsx 组件(教程版本,为简洁省略了部分渲染输出):
import { useState } from 'react'
// Use pre-typed versions of the React-Redux
// `useDispatch` and `useSelector` hooks
import { useAppDispatch, useAppSelector } from '@/app/hooks'
import {
decrement,
increment,
incrementAsync,
incrementByAmount,
incrementIfOdd,
selectCount,
selectStatus
} from './counterSlice'
import styles from './Counter.module.css'
export function Counter() {
// highlight-start
const dispatch = useAppDispatch()
const count = useAppSelector(selectCount)
const status = useAppSelector(selectStatus)
// highlight-end
const [incrementAmount, setIncrementAmount] = useState('2')
const incrementValue = Number(incrementAmount) || 0
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => {
dispatch(decrement())
}}
>
-
</button>
<span aria-label="Count" className={styles.value}>
{count}
</span>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => {
dispatch(increment())
}}
>
+
</button>
{/* omit additional rendering output here */}
</div>
</div>
)
}
仓库中的完整版本见 examples/counter-ts/src/features/counter/Counter.tsx,额外包含 "Add If Odd"(调用 incrementIfOdd thunk)按钮。和早前教程 Part 1 中的纯 React 例子类似,Counter 也是函数组件,用 useState 存数据——但注意:组件里的 count 变量并非来自 useState。
React 内置了 useState、useEffect 等 hook,而第三方库可以基于 React hook 构建自定义 hook。React-Redux 提供了一组自定义 hook,让 React 组件与 Redux store 交互。
用 useSelector 读取数据
useSelector 让组件从 Redux store state 中提取它需要的数据片段。回顾 selector 函数:接收 state、返回 state 的某一部分;我们的 counterSlice.ts 导出了 selectCount 与 selectStatus(见上文"用 Selector 读取数据"一节)。
如果有 store 的直接访问权,可以这样取当前计数:
const count = selectCount(store.getState())
console.log(count)
// 0
但组件不能直接访问 Redux store(不允许在组件文件中导入 store),useSelector 在背后替我们完成与 store 的对话:传入 selector 函数,它替我们调用 someSelector(store.getState()) 并返回结果:
const count = useSelector(selectCount)
也不一定要使用已导出的 selector,可以内联定义:
const countPlusTwo = useSelector((state: RootState) => state.counter.value + 2)
每次有 action 被 dispatch、store 更新后,useSelector 会重新运行 selector 函数;如果返回值与上次不同,useSelector 会确保组件以新值重新渲染。
用 useDispatch 分发 Action
同理,如果有 store 可以直接 store.dispatch(increment());没有 store 引用时,useDispatch 替我们取得 store 的 dispatch 方法:
const dispatch = useDispatch()
之后即可在用户点击按钮等场景 dispatch action:
<button
className={styles.button}
aria-label="Increment value"
onClick={() => {
dispatch(increment())
}}
>
+
</button>
定义预定义类型的 React-Redux Hooks
useSelector 默认要求为每个 selector 声明 (state: RootState)。可以创建预定义类型的版本,避免重复标注:
import { useDispatch, useSelector } from 'react-redux'
import type { AppDispatch, RootState } from './store'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
仓库中的 examples/counter-ts/src/app/hooks.ts 就是这段代码的逐行实现。之后组件中导入 useAppSelector / useAppDispatch 替换原版 hooks 即可。
组件状态与表单:什么该进 Redux
"是不是所有应用状态都必须放进 Redux store?" 答案是:否。跨应用需要的全局状态进 Redux store;只在一处使用的状态留在组件 state 里。
示例中有一个输入框,让用户输入下一次要加到计数器上的数字:
const [incrementAmount, setIncrementAmount] = useState('2')
const incrementValue = Number(incrementAmount) || 0
// 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(incrementValue))}
>
Add Amount
</button>
<button
className={styles.asyncButton}
onClick={() => dispatch(incrementAsync(incrementValue))}
>
Add Async
</button>
</div>
)
我们"可以"在 input 的 onChange 里 dispatch action、把这个字符串存在 reducer 里,但没有收益——这个文本串只在 <Counter> 组件中被使用(即使应用更大、组件更多,也只有 <Counter> 关心它)。所以把它留在 useState 中更合理。同理,一个 isDropdownOpen 之类的布尔标志,其他组件都不关心,就应该留在组件内部。
在 React + Redux 应用中:全局状态进 Redux store,局部状态留在 React 组件里。 拿不准时,可以问自己这几个问题:
- 应用的其他部分是否关心这个数据?
- 是否需要基于它进一步派生数据?
- 同一份数据是否驱动多个组件?
- 是否有价值把这个状态恢复到某个历史时间点(时间旅行调试)?
- 是否想缓存数据(state 里已有就直接用,不重复请求)?
- 热更新 UI 组件(组件被替换时会丢失内部状态)时,是否希望数据保持一致?
这也是思考"Redux 中表单状态"的一般性范例:大部分表单状态不应该放进 Redux;编辑期间把数据留在表单组件里,用户完成后再 dispatch action 更新 store。
另外注意:incrementAsync thunk 也在这里被使用了,且与普通 action creator 的用法一模一样——组件不关心 dispatch 的是普通 action 还是启动异步逻辑,它只知道"点击按钮就 dispatch 了某个东西"。
提供 Store(<Provider>)
组件可以用 useSelector / useDispatch 与 store 对话,但我们从未在组件中导入 store——这些 hook 怎么知道要访问哪个 store?回到应用入口(教程版本为 main.tsx):
import React from 'react'
import { createRoot } from 'react-dom/client'
// highlight-next-line
import { Provider } from 'react-redux'
import App from './App'
import { store } from './app/store'
import './index.css'
const container = document.getElementById('root')!
const root = createRoot(container)
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
)
仓库中的实际入口 examples/counter-ts/src/index.tsx 结构相同(文件名为 index.tsx)。关键点:
root.render(<App />)负责让 React 开始渲染根组件<App>;- 为了让
useSelector等 hook 正常工作,需要用<Provider>组件在背后传递 Redux store,使组件树中的 hooks 能访问到它; - store 已经在
app/store.ts中创建,导入后用<Provider store={store}>包住整个<App>; - 此后,任何调用
useSelector/useDispatch的 React 组件访问的都是传给<Provider>的那个 store。
至此,应用的每一块拼图都已就位:counterSlice.ts 定义逻辑 → store.ts 组装 store 并导出类型 → hooks.ts 提供预类型 hooks → Counter.tsx 读写状态 → 入口文件用 <Provider> 注入 store。
本篇小结
- 用 Redux Toolkit 的
configureStore创建 Redux storeconfigureStore以命名参数接收reducer函数(或 reducer 对象);configureStore自动为 store 配置合理的默认设置(含 thunk 等中间件与 DevTools 接入)。
- Redux 逻辑通常组织在名为 "slice" 的文件中
- 一个 slice 包含与某个功能/state 区段相关的 reducer 逻辑与 action;
createSlice为每个 reducer 函数自动生成 action creator 与 action type(name+ reducer 键名)。
- Reducer 必须遵守特定规则
- 只能基于
state与action计算新 state; - 必须通过复制现有 state 做"不可变更新";
- 不能包含异步逻辑或其他"副作用";
createSlice借助 Immer 允许以"mutating"风格书写不可变更新(脱离 Immer 则绝不可 mutation)。
- 只能基于
- 读取 state 靠 "selector" 函数
- 以
(state: RootState)为参数,返回 state 中的值或派生新值; - 可以写在 slice 文件中,也可以内联在
useSelector里,并应把握使用平衡。
- 以
- 异步逻辑通常写在 "thunk" 中
- thunk 接收
dispatch与getState; configureStore默认启用redux-thunk中间件;createAsyncThunk可自动生成pending/fulfilled/rejectedaction,在extraReducers中处理。
- thunk 接收
- React-Redux 让 React 组件与 Redux store 交互
- 用
<Provider store={store}>包住应用后,所有组件都能使用该 store; useSelector读取 store 值,useDispatch分发 action;- TypeScript 场景下创建预类型
useAppSelector/useAppDispatch; - 全局状态进 Redux store,局部状态留在 React 组件中。
- 用
下一步
示例应用虽小,却展示了 React + Redux 应用的所有关键部件如何协同工作。接下来将在教程 Part 3: Basic Redux Data Flow 中开始动手构建一个更大的示例应用,并沿途覆盖正确使用 Redux 所需的全部关键概念。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00


