- 前端
【免费下载链接】redux-thunk
Thunk middleware for Redux
本文以开源仓库 redux-thunk 的官方 README 为主体,结合 中间件源码、类型定义 与 单元测试 展开讲解。你将系统掌握 Redux Thunk 是什么、如何在 Redux Toolkit 与手写 store 两种场景下安装配置、如何注入自定义参数
extraArgument、以及如何利用 thunk 组合 Promise 编排复杂的异步控制流,最终具备在真实项目中落地 thunk 异步逻辑的完整能力。
一句话认识 Redux Thunk
Redux Thunk 是 Redux 的Thunk 中间件(middleware)。它允许你编写"内部带有逻辑的函数"作为 action 派发,这些函数可以访问 Redux store 的dispatch与getState方法,从而在 Redux 生态内自由地执行异步逻辑与复杂的同步逻辑。仓库在 package.json 中将其定位为 "Thunk middleware for Redux",当前版本为3.1.0,采用 MIT 协议开源。
从中间件的标准视角看,Redux 官方将中间件定义为"位于dispatch与 store 之间、用于扩展 store 能力的函数层"。Redux Thunk 正是这样一层:它拦截所有派发到 store 的 action,发现其中"实际上是函数"的项,便不再当作普通 action 处理,而是立即调用它,并把 store 的dispatch、getState(以及可选的extraArgument)注入进去。
什么是 "thunk"?
一个thunk是"用一个函数包裹表达式以延迟其求值"的函数。
// 1 + 2 的计算是立即发生的 // x === 3 let x = 1 + 2 // 1 + 2 的计算被延迟了 // foo 可以在之后被调用以执行计算 // foo 就是一个 thunk! let foo = () => 1 + 2x在定义时立刻算出结果3,而foo只是"准备好的一段计算",直到调用foo()才会真正求值。这个概念正是 Redux Thunk 的核心思想:把"派发动作"这个动作本身延迟到合适的时机执行。"thunk" 一词据说源自 "think" 的戏谑过去式,意指"思考后延后处理的产物"。
安装与初始化配置
Redux Thunk 有两种典型的接入方式,取决于你使用的是 Redux Toolkit 还是基础版 ReduxcreateStoreAPI。
使用 Redux Toolkit:零配置自动集成
如果你按官方推荐使用Redux Toolkit,那么无需额外安装任何东西——RTK 的configureStoreAPI 默认就会添加 thunk 中间件:
import { configureStore } from '@reduxjs/toolkit' import todosReducer from './features/todos/todosSlice' import filtersReducer from './features/filters/filtersSlice' const store = configureStore({ reducer: { todos: todosReducer, filters: filtersReducer } }) // thunk 中间件已被自动添加注意:thunk 中间件并不包含在 Redux 核心库中,而是被@reduxjs/toolkit包默认启用。因此只要 store 由configureStore创建,store.dispatch就天然支持派发 thunk 函数。
手动接入:createStore + applyMiddleware
如果你使用基础版 ReduxcreateStoreAPI,需要手动完成两步。
第一步,安装依赖:
npm install redux-thunk yarn add redux-thunk第二步,引入中间件。请注意,thunk 中间件是命名导出(named export),不是默认导出。在 ES Modules 环境中:
import { thunk } from 'redux-thunk'在 CommonJS 环境中:
const { thunk } = require('redux-thunk')第三步,通过applyMiddleware()启用:
import { createStore, applyMiddleware } from 'redux' import { thunk } from 'redux-thunk' import rootReducer from './reducers/index' const store = createStore(rootReducer, applyMiddleware(thunk))包结构与模块格式说明
从仓库 package.json 可以看出,redux-thunk@3.1.0的发布结构考虑得非常细致:
exports字段为不同环境提供了对应的入口:types指向dist/redux-thunk.d.ts,import指向dist/redux-thunk.mjs(ESM),default指向dist/cjs/redux-thunk.cjs(CommonJS);sideEffects: false声明模块无副作用,便于打包器做 tree-shaking;- 声明
peerDependencies: { "redux": "^5.0.0" },即要求宿主项目提供 Redux 5.x 作为对等依赖; - 构建脚本通过 tsup.config.ts 生成 ESM(
.mjs)、兼容 Webpack 4 的 legacy ESM(.js)以及 CJS(.cjs)三套产物,入口统一为 src/index.ts。
注入自定义参数:extraArgument
自2.1.0版本起,Redux Thunk 支持向 thunk 中间件注入一个自定义参数(extraArgument)。它典型地用于"API 服务层"场景:在测试中可以把真实服务无缝替换成 mock 服务,而业务代码无需改动。
方式一:Redux Toolkit 的 getDefaultMiddleware
在configureStore内部的getDefaultMiddleware回调中传入自定义extraArgument:
import { configureStore } from '@reduxjs/toolkit' import rootReducer from './reducer' import { myCustomApiService } from './api' const store = configureStore({ reducer: rootReducer, middleware: getDefaultMiddleware => getDefaultMiddleware({ thunk: { extraArgument: myCustomApiService } }) }) // 稍后使用 function fetchUser(id) { // extraArgument 是 thunk 函数的第三个参数 return (dispatch, getState, api) => { // 你可以在这里使用 api } }方式二:传入多个值
如果需要同时注入多个值,把它们合并进一个对象即可:
const store = configureStore({ reducer: rootReducer, middleware: getDefaultMiddleware => getDefaultMiddleware({ thunk: { extraArgument: { api: myCustomApiService, otherValue: 42 } } }) }) // 稍后使用 function fetchUser(id) { return (dispatch, getState, { api, otherValue }) => { // 你可以在这里使用 api 和 otherValue } }方式三:手写 store 场景下的 withExtraArgument
如果你手动创建 store,则应使用命名导出withExtraArgument()函数来生成携带自定义参数的 thunk 中间件:
const store = createStore(reducer, applyMiddleware(withExtraArgument(api)))从源码看,withExtraArgument并非独立实现,而是同一个工厂函数的别名。在 src/index.ts 中:
function createThunkMiddleware< State = any, BasicAction extends Action = AnyAction, ExtraThunkArg = undefined >(extraArgument?: ExtraThunkArg) { // ... return middleware } export const thunk = createThunkMiddleware() // 导出工厂函数,便于用户创建注入自定义 "extra arg" 的定制版本 export const withExtraArgument = createThunkMiddleware也就是说,默认的thunk等价于createThunkMiddleware()(不注入任何参数),而withExtraArgument(api)等价于createThunkMiddleware(api)。在 单元测试 中,withExtraArgument(extraArg)生成中间件后,thunk 函数收到的第三个参数与注入值严格相等(expect(arg).toBe(extraArg)),印证了这一行为。
为什么需要 Thunk?
使用普通的基础版 Redux store,你只能通过派发 action 进行简单的同步更新。中间件扩展了 store 的能力,让你能够编写与 store 交互的异步逻辑。
Thunks 是 Redux基础副作用逻辑的推荐中间件方案,覆盖两类场景:
- 需要访问 store 的复杂同步逻辑;
- 类似 AJAX 请求的简单异步逻辑。
Redux 官方文档有专门的《Writing Logic with Thunks》页面,详细讲解 thunk 存在的原因、thunk 中间件的工作机制以及实用模式;社区中关于"为什么异步流程需要中间件"的经典讨论也系统地介绍了从内联异步调用、异步 action creator 到 thunk 中间件的渐进演进过程。
核心动机:让 action creator 返回函数
Redux Thunk 允许你编写返回函数而非 action 的 action creator。这个 thunk 函数可以延迟 action 的派发,也可以只在满足某个条件时才派发。内层函数会接收到 store 的dispatch和getState两个方法作为参数。
异步派发示例
一个返回函数以执行异步派发的 action creator:
const INCREMENT_COUNTER = 'INCREMENT_COUNTER' function increment() { return { type: INCREMENT_COUNTER } } function incrementAsync() { return dispatch => { setTimeout(() => { // 太好了!可以在 dispatch 中调用同步或异步 action dispatch(increment()) }, 1000) } }这里incrementAsync()返回的函数就是 thunk:1 秒后通过dispatch派发一个普通的increment()action。调用方store.dispatch(incrementAsync())与派发普通 action 的写法完全一致——中间件让 thunk "伪装"成了普通 action。
条件派发示例
一个返回函数以执行条件派发的 action creator:
function incrementIfOdd() { return (dispatch, getState) => { const { counter } = getState() if (counter % 2 === 0) { return } dispatch(increment()) } }incrementIfOdd()借助getState读取当前状态,仅在计数器为奇数时才真正派发increment()——这就是"条件延迟派发"的典型形态。
中间件的判定机制(源码级)
为什么函数能被dispatch接收?答案在 src/index.ts 的中间件核心实现中:
const middleware: ThunkMiddleware<State, BasicAction, ExtraThunkArg> = ({ dispatch, getState }) => next => action => { // thunk 中间件会检查所有传给 store.dispatch 的值。 // 如果这个 "action" 实际上是个函数,就调用它并返回结果。 if (typeof action === 'function') { // 注入 store 的 dispatch 和 getState 方法,以及任何 "extra arg" return action(dispatch, getState, extraArgument) } // 否则,像普通中间件一样把 action 继续传给下一个中间件 return next(action) }三段式柯里化是 Redux 自定义中间件的标准结构:第一层接收{ dispatch, getState }(中间件 API),第二层接收next(下一个中间件),第三层接收action。唯一的判别逻辑是typeof action === 'function'——是函数就调用并注入三个参数,否则原样交给next。整套中间件逻辑只有十几行,这正是 thunk 之所以"轻量且易于理解"的根本原因。
组合:用 Promise 编排异步控制流
Thunk 最有价值的能力在于组合(Composition):内层函数的任何返回值都会成为dispatch本身的返回值。这为"thunk action creator 互相派发、返回 Promise 互相等待"的异步控制流编排提供了极大便利。
注意:以下组合 API 要求
redux@>=3.1.0(当前仓库声明的对等依赖为redux@^5.0.0)。
import { createStore, applyMiddleware } from 'redux' import { thunk } from 'redux-thunk' import rootReducer from './reducers' const store = createStore(rootReducer, applyMiddleware(thunk)) function fetchSecretSauce() { return fetch('https://www.google.com/search?q=secret+sauce') } // 这些是你已经见过的普通 action creator。 // 它们返回的 action 无需任何中间件即可被派发。 // 但它们只表达 "事实",不表达 "异步流程"。 function makeASandwich(forPerson, secretSauce) { return { type: 'MAKE_SANDWICH', forPerson, secretSauce } } function apologize(fromPerson, toPerson, error) { return { type: 'APOLOGIZE', fromPerson, toPerson, error } } function withdrawMoney(amount) { return { type: 'WITHDRAW', amount } } // 即使没有中间件,你也可以派发一个 action: store.dispatch(withdrawMoney(100)) // 但当你需要发起异步动作(如 API 调用或路由跳转)时该怎么办? // 认识一下 thunks。 // 在此语境下,thunk 是一个可以被派发以执行异步活动、 // 并能派发 action 和读取状态的函数。 // 下面是一个返回 thunk 的 action creator: function makeASandwichWithSecretSauce(forPerson) { // 我们可以在这里反转控制权——返回一个函数,即 "thunk"。 // 当这个函数被传给 dispatch 时,thunk 中间件会拦截它, // 并以 dispatch 和 getState 作为参数调用它。 // 这赋予了 thunk 函数运行逻辑、同时与 store 交互的能力。 return function (dispatch) { return fetchSecretSauce().then( sauce => dispatch(makeASandwich(forPerson, sauce)), error => dispatch(apologize('The Sandwich Shop', forPerson, error)) ) } } // Thunk 中间件让我可以像派发普通 action 一样派发 thunk 异步 action! store.dispatch(makeASandwichWithSecretSauce('Me')) // 它甚至会把 thunk 的返回值从 dispatch 中透传出来, // 因此只要我返回 Promise,就可以一直链式串联下去。 store.dispatch(makeASandwichWithSecretSauce('My partner')).then(() => { console.log('Done!') }) // 事实上,我可以编写从其他 action creator 派发普通 action 和异步 action // 的 action creator,并用 Promise 构建我的控制流。 function makeSandwichesForEverybody() { return function (dispatch, getState) { if (!getState().sandwiches.isShopOpen) { // 你不一定非要返回 Promise,但这是一种方便约定—— // 这样调用方总能对异步 dispatch 的结果调用 .then()。 return Promise.resolve() } // 我们既可以派发普通对象 action,也可以派发其他 thunk, // 这让我们能在单一流程中组合异步动作。 return dispatch(makeASandwichWithSecretSauce('My Grandma')) .then(() => Promise.all([ dispatch(makeASandwichWithSecretSauce('Me')), dispatch(makeASandwichWithSecretSauce('My wife')) ]) ) .then(() => dispatch(makeASandwichWithSecretSauce('Our kids'))) .then(() => dispatch( getState().myMoney > 42 ? withdrawMoney(42) : apologize('Me', 'The Sandwich Shop') ) ) } } // 这对服务端渲染非常有用,因为我可以一直等到数据就绪, // 再同步渲染整个应用。 store .dispatch(makeSandwichesForEverybody()) .then(() => response.send(ReactDOMServer.renderToString(<MyApp store={store} />)) ) // 我也可以在组件的 props 变化时派发一个 thunk 异步 action 来加载缺失的数据。 import { connect } from 'react-redux' import { Component } from 'react' class SandwichShop extends Component { componentDidMount() { this.props.dispatch(makeASandwichWithSecretSauce(this.props.forPerson)) } componentDidUpdate(prevProps) { if (prevProps.forPerson !== this.props.forPerson) { this.props.dispatch(makeASandwichWithSecretSauce(this.props.forPerson)) } } render() { return <p>{this.props.sandwiches.join('mustard')}</p> } } export default connect(state => ({ sandwiches: state.sandwiches }))(SandwichShop)这个示例浓缩了 thunk 组合的全部要点:
- 透传返回值:
dispatch返回 thunk 的返回值,Promise 得以跨 thunk 传递; - 并发与串行混合:
Promise.all并发派发多个 thunk,then串行编排依赖关系; - 状态参与决策:
getState().isShopOpen、getState().myMoney在流程中实时读取最新状态; - 服务端渲染等待数据:先
dispatch再渲染,保证首屏数据完整; - 组件驱动补数据:
componentDidMount/componentDidUpdate中按需派发 thunk。
返回值透传的测试验证
"任何返回值都会成为dispatch的返回值"这一行为,在 test/index.test.ts 中有直接验证:当 action 是函数时,actionHandler(() => expected)的结果等于expected(expect(outcome).toBe(expected));同时该测试还验证了 thunk 是同步调用的(调用后副作用立即生效,expect(mutated).toBe(1)),以及普通对象 action 会原样透传给next、非函数的next返回值也会被原样返回。
TypeScript 类型系统支持
redux-thunk 的完整类型定义位于 src/types.ts,并在 src/index.ts 中全部重新导出。核心类型包括:
ThunkAction<ReturnType, State, ExtraThunkArg, BasicAction>:描述一个 thunk 函数签名,接收dispatch、getState、extraArgument三个参数并返回ReturnType。它重载了dispatch的类型,使其既能接收 thunk 函数(返回 thunk 的返回值),也能接收标准 action 对象(返回 action 本身);ThunkDispatch<State, ExtraThunkArg, BasicAction>:thunk 中间件改造后的dispatch类型,含三个重载:thunk 函数重载、普通 action 重载,以及为规避 TypeScript 泛型推断问题(对应社区 issue #248)而增加的联合重载;ThunkActionDispatch<ActionCreator>:描述 thunk action creator 被bindActionCreators()包装后的签名——接收外层函数的参数,返回内层 thunk 函数的返回值类型;ThunkMiddleware<State, BasicAction, ExtraThunkArg>:中间件本身的类型,默认State = any、BasicAction = AnyAction、ExtraThunkArg = undefined。
typescript_test/index.test-d.ts 中的类型测试(通过tsc --noEmit运行,脚本见 package.json 的type-tests)覆盖了:dispatch可调用 thunk 与普通 action、getState()返回精确的 State 类型、withExtraArgument('bar')后 thunk 第三参数被推断为string、以及bindActionCreators场景下ThunkActionDispatch的返回值类型推导。若希望store.dispatch获得这些精确类型,可像类型测试中那样将 store 显式声明为createStore(reducer, applyMiddleware(thunk as ThunkMiddleware<State, Actions>))。
源码与测试:中间件的完整行为契约
综合 src/index.ts、test/index.test.ts 与 typescript_test/index.test-d.ts,可以归纳出 redux-thunk 的完整行为契约:
| 行为 | 源码位置 | 测试验证 |
|---|---|---|
| 中间件为三段柯里化结构(API → next → action) | src/index.ts | test/index.test.ts:nextHandler、actionHandler均为函数 |
typeof action === 'function'时调用 thunk 并注入dispatch/getState/extraArgument | src/index.ts | test/index.test.ts、L87-L100 |
非函数 action 原样透传给next | src/index.ts | test/index.test.ts |
thunk 返回值成为dispatch返回值 | src/index.ts | test/index.test.ts |
| thunk 被同步调用 | src/index.ts | test/index.test.ts |
| 未传入中间件 API 对象时抛错 | src/index.ts(解构undefined) | test/index.test.ts |
withExtraArgument(arg)等价于定制化的 thunk 工厂 | src/index.ts | test/index.test.ts |
整套测试由 Vitest 驱动(vitest --run --typecheck,配置见 vitest.config.mts 与 package.json)。
结语
Redux Thunk 以极其精简的实现(核心中间件仅十余行)解决了 Redux 异步逻辑的基础难题:用函数延迟 action 的派发,借助dispatch与getState让异步逻辑与 store 深度交互,再通过返回值透传实现 Promise 化的流程编排。它默认集成于 Redux Toolkit,也可通过applyMiddleware手动接入;withExtraArgument则让依赖注入与测试替身变得轻而易举。当你面对 AJAX 请求、条件派发、串行/并行的异步流程、服务端渲染数据预取等场景时,thunk 都是值得优先选择的轻量方案——这也正是它被 Redux 官方推荐为"基础副作用逻辑默认中间件"的原因所在。项目以 MIT 协议开源,你可以在此基础上自由地集成、扩展与二次分发。
- 前端
【免费下载链接】redux-thunk
Thunk middleware for Redux
相关推荐
DeepSeek Coder 33B Base实战指南:从安装到代码生成的完整教程
DeepSeek Coder 33B Base实战指南:从安装到代码生成的完整教程 想要体验最先进的代码生成AI模型吗?DeepSeek Coder 33B B
Redux 项目中的 Thunk 中间件:异步逻辑处理指南
Redux 项目中的 Thunk 中间件:异步逻辑处理指南 什么是 Thunk? 在编程领域,"thunk" 是一个专业术语,指的是 一段延迟执行的代码 。在
前端范式变革:下一代AI智能体如何重塑企业智能化生态
范式变革:下一代AI智能体如何重塑企业智能化生态 在人工智能技术快速演进的今天,企业面临着从传统自动化向智能自主化转型的关键抉择。AI智能体生态系统正经历着从单
AI Agent人工智能
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考