首页
/ Redux Essentials 详解(二):React + Redux Toolkit 应用结构——Store、Slice 与状态管理规则

Redux Essentials 详解(二):React + Redux Toolkit 应用结构——Store、Slice 与状态管理规则

2026-09-04 19:50:44作者:冯梦姬Eddie

本篇基于 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>useSelectoruseDispatch 把 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 的初始状态:

Redux DevTools 中计数器应用的初始状态:counter.value 为 0、status 为 idle

初始应用状态形如:

{
  counter: {
    value: 0,
    status: 'idle'
  }
}

接下来按顺序操作应用,观察 DevTools 的变化:

  1. 点击 "+" 按钮,切换到 Redux DevTools 的 "Diff" 标签页:

Redux DevTools 的 Diff 标签页:展示 counter/increment 动作将 state.counter.value 从 0 改为 1

此时可以确认两件关键事实:

  • 点击 "+" 按钮时,一个 type"counter/increment" 的 action 被 dispatch 到 store;
  • 该 action 处理时,state.counter.value 字段从 0 变成了 1
  1. 继续操作:再点一次 "+"(值变为 2);点一次 "-"(值变为 1);点击 "Add Amount" 按钮(值变为 3);把输入框中的 "2" 改为 "3";点击 "Add Async" 按钮——会看到按钮上出现进度条,几秒后显示值变为 6。

  2. 回到 Redux DevTools,此时应该能看到一共 5 次 dispatch 的 action(每次点击对应一次)。选中最后一条 "counter/incrementByAmount",点击右侧 "Action" 标签页,可以看到该 action 对象形如:

{
  type: 'counter/incrementByAmount',
  payload: 3
}

切换 "Diff" 标签页,可以看到 state.counter.value 因为这个 action 从 3 变成了 6

  1. 再点击右上角的 "Trace" 标签页,可以看到一段 JavaScript 函数调用栈,其中高亮的那一行正是 <Counter> 组件中 dispatch 该 action 的代码位置,方便追踪"是哪个代码触发了这次状态变化":

Redux DevTools 的 Trace 标签页:展示 counter/incrementByAmount 动作的调用栈,高亮 Counter 组件中的 dispatch 行

能够实时看到应用内部发生了什么、状态如何随时间变化,正是 Redux DevTools 最强大的能力之一。

应用的关键文件清单

教程将示例应用的关键文件归纳如下(对照本仓库的 examples/counter-ts 目录,可以看到同一结构在 Create React App 版本中的落地):

  • /src
    • main.tsx:应用入口(仓库示例中对应 index.tsx
    • App.tsx:顶层 React 组件
    • /app
      • store.ts:创建 Redux store 实例
      • hooks.ts:导出预定义类型的 React-Redux hooks
    • /features
      • /counter
        • Counter.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 类型导出RootStateAppDispatchAppThunk 等基于 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.usersstate.postsstate.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 实现印证了上述行为,还包含两条值得了解的机制:

  1. 初始化校验(见 src/combineReducers.ts#L62-L94):assertReducerShape 会先用 INIT action 探测每个 slice reducer,若初始化时返回 undefined 直接抛错——因为 slice reducer 在 stateundefined 时必须返回初始状态。
  2. 变更短路优化(见 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 返回 undefinedcombineReducers 会抛出明确的错误(提示"必须显式返回 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}

除了 namecreateSlice 还需要初始 state(initialState),这样 reducer 第一次被调用时就有 state 可用:这里是一个 value: 0status: 'idle' 的对象。reducers 中定义了三个 reducer 函数,恰好对应 UI 上三种按钮点击产生的三种 action type。

仓库示例中的同一份 slice

仓库中 examples/counter-ts/src/features/counter/counterSlice.ts 与教程片段基本一致(额外包含后文要讲的 incrementAsyncincrementIfOdd 与两个 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 必须始终遵守三条规则:

  • 只能基于 stateaction 两个参数计算新 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 的 createSlicecreateReducer 中才能写"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 函数:接收 dispatchgetState 作为参数;
  • 外层 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。这里同样可以轻松放入 setTimeoutawait 等异步逻辑。用法与普通 action creator 完全一致:

store.dispatch(incrementIfOdd(6))

注意前提:使用 thunk 要求 store 创建时加入了 redux-thunk 中间件(一种 Redux 插件)。configureStore 已自动完成这一步,所以可以直接用。从源码看,未经中间件包装的 store 在 src/createStore.ts#L270-L277 中对非普通对象的 dispatch 直接抛错(前文已展示),而中间件会重写 dispatch 链来拦截这类值——这正是 thunks "必须依赖中间件"的底层原因。

编写 thunk 时,dispatchgetState 的类型要正确。可以逐个标注 (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 fulfilledrejected 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 时,在 createSliceextraReducers 中处理它生成的 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 后续链。这样,我们就可以在任意同步/异步代码中同时持有 dispatchgetState

仓库核心包中同样提供了中间件组合的实现,见 src/applyMiddleware.ts(将多个中间件组合成 store enhancer);thunk 的详细用法会在教程 Part 5: Async Logic and Data Fetching 继续展开,背景讨论见 Writing Logic in ThunksFAQ: 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 内置了 useStateuseEffect 等 hook,而第三方库可以基于 React hook 构建自定义 hook。React-Redux 提供了一组自定义 hook,让 React 组件与 Redux store 交互。

useSelector 读取数据

useSelector 让组件从 Redux store state 中提取它需要的数据片段。回顾 selector 函数:接收 state、返回 state 的某一部分;我们的 counterSlice.ts 导出了 selectCountselectStatus(见上文"用 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 store
    • configureStore 以命名参数接收 reducer 函数(或 reducer 对象);
    • configureStore 自动为 store 配置合理的默认设置(含 thunk 等中间件与 DevTools 接入)。
  • Redux 逻辑通常组织在名为 "slice" 的文件中
    • 一个 slice 包含与某个功能/state 区段相关的 reducer 逻辑与 action;
    • createSlice 为每个 reducer 函数自动生成 action creator 与 action type(name + reducer 键名)。
  • Reducer 必须遵守特定规则
    • 只能基于 stateaction 计算新 state;
    • 必须通过复制现有 state 做"不可变更新";
    • 不能包含异步逻辑或其他"副作用";
    • createSlice 借助 Immer 允许以"mutating"风格书写不可变更新(脱离 Immer 则绝不可 mutation)。
  • 读取 state 靠 "selector" 函数
    • (state: RootState) 为参数,返回 state 中的值或派生新值;
    • 可以写在 slice 文件中,也可以内联在 useSelector 里,并应把握使用平衡。
  • 异步逻辑通常写在 "thunk" 中
    • thunk 接收 dispatchgetState
    • configureStore 默认启用 redux-thunk 中间件;
    • createAsyncThunk 可自动生成 pending/fulfilled/rejected action,在 extraReducers 中处理。
  • 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 所需的全部关键概念。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
903
1.82 K
docsdocs
暂无描述
Markdown
888
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.51 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341