通过 React、Angular、Vue 或 React Native 应用生成的用户界面是其状态的函数。对于许多前端开发者来说,Redux Toolkit 是完成任务的理想工具。
Redux Toolkit 是 Redux 生态系统的一部分。多年来,Redux 一直是管理复杂应用程序状态的首选解决方案,但 Redux Toolkit 填补了 Redux 与易用性之间的鸿沟。大约一年多以来,Redux Toolkit 一直是在你的应用程序中使用 Redux 的官方推荐方法。
在这个采用指南中,我们将探讨 Redux Toolkit 及其功能、优缺点。我们还将讨论其使用案例和替代方案,以帮助你更好地评估是否在你的下一个项目中利用这个工具。
Redux Toolkit 是什么?
Redux Toolkit(RTK)是高效 Redux 开发的官方、有主张的工具集。
在 RTK 之前,开发者必须手动编写 Redux 存储,使用大量样板代码。这包括设置减速器、操作、选择器、操作和中间件,只是为了运行一个基本的存储。RTK 通过提供一组实用函数来简化你使用 Redux 的标准方式,处理了这些痛苦的任务。
Redux 团队知道开发者在 Redux 的复杂性方面存在问题。因此,他们着手创建一个解决方案,以简化 Redux 工作流程,并使状态管理对开发者和 React Toolkit 更简单。
最初于 2019 年发布,Redux Toolkit 旨在封装最佳实践、常见模式和有用工具,以改进 Redux 开发体验。
Redux Toolkit 如何工作?
Redux Toolkit 是我们所熟知的 Redux 原则的一个封装器。它提供了简化开发者常常讨厌的任务的实用工具。以下是一个快速的概述:
- 简化存储设置:RTK 的
configureStore
函数简化了使用预构建配置和中间件创建 Redux 存储的过程。 - 自动减速器和操作创建:
createSlice
函数允许你简单定义减速器和操作,而不需要所有旧的样板代码。 - 直观的状态更新:RTK 与 Immer 库 集成,这意味着你可以在不手动处理不可变性的情况下写入状态。
- 中间件:RTK 配备了 Redux Thunk 用于异步操作和 Reselect 用于优化选择器函数。
- 强大的工具:React Toolkit 还配备了诸如 RTK Query 用于数据获取和缓存的实用工具。
Redux 团队最近发布了 Redux Toolkit 的 2.0 版本,增加了以下功能:
- 一个新的
combineSlices
方法,将惰性加载片减速器以更好地进行 代码拆分。 - 在运行时添加中间件的能力。
- 减速器中的异步 thunk 支持。
- 将选择器作为切片的一部分。
为什么使用 Redux Toolkit?
让我们诚实一点:Redux 需要改变。它带来了过多的样板代码、复杂的设置,以及一个学习曲线,即使对于经验丰富的开发者在我们称之为前端开发的不断变化的领域中也可能陡峭。
Redux Toolkit 是 Redux 所需要的改变。它在正反对比中增加了更多“优点”。以下是使用 Redux Toolkit 的一些原因:
- 便利性:不再需要为减速器、操作、选择器和常量编写无休止的样板代码。或者通过搜索名称、常量,最终找到减速器中的问题 —— 然后再次通过代码回溯找到问题。RTK 的
createSlice
承担了所有繁重的工作,为你节省了时间和理智。 - 性能:Redux Toolkit 继承了 Redux 的单一真相源方法,并添加了诸如记忆选择器和 Immer 等工具,让你在不为每个更改创建新对象的开销下以不可变的方式修改状态。
- 易用性:RTK 使状态更新更加直观,并通过 Immer 减少了错误的风险。API 清晰,文档是你使用它所需要的全部内容,与纯 Redux 相比,学习曲线很平缓。在早期试图改进 Redux 的过程中,我看过许多类似于 RTK 的选项,尝试了一些,并放弃了。自从接触到 Redux Toolkit 以来,我已经使用了几年,完全是支持者!
- 社区和生态系统:只因为每个人似乎都在使用一个库,并不意味着你也应该这样做。然而,有一个大型社区支持着 RTK,这是一个运行良好的工具。如果你遇到问题或在实现 RTK 方面遇到困难,谷歌搜索通常会找到其他人不仅感受到你的痛苦,而且已经找到了你问题的解决方案。
- 文档:有些库在文档方面做得很好,因为它们有大量可用资源,但格式混乱且难以理解。我只想要一些真实的代码示例来展示我如何使用东西。然而,Redux Toolkit 的文档 既全面又组织良好,包含大量示例。文档将引导你从入门到探索用例、参数、异常等更多内容,甚至是我还没有必要查看的内容。
- 集成:RTK 与其他工具(尤其是 React 和 TypeScript)兼容良好,并减少了创建手动类型定义的必要性,同时为你提供了类型安全性。
即便如此,Redux Toolkit 并不是一刀切的解决方案。以下是你可能重新考虑在你的应用程序中使用 RTK 的原因:
- 捆绑大小:与纯 Redux 相比,RTK 会给你的捆绑包增加一些负担。但让我们诚实一点,与 现代应用程序的整体大小 及其带来的好处相比,这只是一个小代价。
- 定制:虽然它消除了额外的开销,但 RTK 添加了一个抽象层。如果你需要对 Redux 所做的一切进行深度控制,纯 Redux 可能会给你更多的灵活性。但对于大多数应用程序,你不需要深入到这个程度。
- 复杂性:虽然 Redux Toolkit 比纯 Redux 更容易,但它仍然有一个学习曲线,但有许多资源和一个庞大的社区来支持你。起初可能会有挑战,但一旦你掌握了它,你会看到它的价值。
- 对简单应用程序过度:对于一些应用程序,你可能不需要 Redux、Redux Toolkit 或任何第三方状态管理库。
了解的关键 Redux Toolkit 功能
Redux Toolkit 提供了一些强大的功能,使得使用 Redux 进行状态管理比几年前容易得多。以下是一些你需要了解的重要功能,以开始使用 RTK。
配置 Redux 存储
你的 Redux 存储是应用程序状态的唯一数据源。过去,你会使用 createStore
创建这个存储。现在,推荐的方法是使用 configureStore
,它不仅会为你创建存储,还会接受减速器函数。
下面是 configureStore
的基本示例:
import { configureStore } from '@reduxjs/toolkit';
import appReducer from 'store/reducers/appSlice';
import cartReducer from 'store/reducers/cartSlice';
const store = configureStore({
reducer: {
// 在这里添加你的减速器,或先合并为 rootReducer
app: appReducer,
cart: cartReducer
},
// 我们稍后再看如何添加中间件
// middleware: (getDefaultMiddleware) => ...
});
// 使用这些类型进行简单的类型声明
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<ReturnType, RootState, unknown, Action<string>>;
// 在整个应用程序中使用它们,而不是简单地使用 `useDispatch` 和 `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
使用减速器和操作创建切片
从上面的示例代码中可以看到,减速器是从“切片”文件中导入的。这些文件是大部分神奇发生的地方。切片是一个包含了单个应用程序功能的 Redux 减速器逻辑和操作的集合 —— 在 v2.0 之后还有更多。
下面是一个基本的切片:
import { createSlice } from '@reduxjs/toolkit';
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartState {
items: CartItem[];
total: number;
}
const initialState: CartState = {
items: [],
total: 0,
};
const cartSlice = createSlice({
// 给切片命名
name: 'cart',
// 设置初始状态
initialState,
// 将减速器操作添加到此对象中
reducers: {
addItem(state, action: { payload: CartItem }) {
const newItem = action.payload;
state.items.push(newItem);
state.total += newItem.price;
},
removeItem(state, action: { payload: string }) {
const itemId = action.payload;
const itemIndex = state.items.findIndex((item) => item.id === itemId);
if (itemIndex !== -1) {
state.items.splice(itemIndex, 1);
state.total -= state.items[itemIndex].price;
}
},
updateQuantity(state, action: { payload: { itemId: string; newQuantity: number } }) {
const { itemId, newQuantity } = action.payload;
const item = state.items.find((item) => item.id === itemId);
if (item) {
item.quantity = newQuantity;
state.total += (newQuantity - item.quantity) * item.price;
}
},
},
});
// 导出操作以在应用程序中使用
export const { addItem, removeItem, updateQuantity } = cartSlice.actions;
// 导出减速器以添加到存储中(第一个示例)
export default cartSlice.reducer;
在切片中添加 thunk
上面的代码将允许我们在应用程序中更新 Redux 状态。我们只需分发我们从切片文件中导出的操作。但是如果我们想要使用 API 调用的数据来填充状态呢?这也很简单。
Redux Toolkit 配备了 Redux Thunk。如果你使用的是 v2.0 或更高版本,你可以直接将 thunk 添加到切片中。Redux 中的 thunk 封装了异步代码。下面是在切片中使用它们的方法,带有注释解释重要部分:
const cartSlice = createSlice({
name: 'cart',
initialState,
// 注意:我们将这个更改为一个回调以传递创建进来。
reducers: (create) => ({
// 注意:标准减速器现在必须使用回调语法。
// 与上一个示例进行比较。
addItem: create.reducer(state, action: { payload: CartItem }) {
const newItem = action.payload;
state.items.push(newItem);
state.total += newItem.price;
},
// 要将 thunk 添加到你的减速器中,请使用 create.AsyncThunk 而不是 create.reducer
// create.AsyncThunk 的第一个参数是实际的 thunk。
fetchCartData: create.AsyncThunk<CartItem[], void, {}>(
'cart/fetchCartData',
async () => {
const response = await fetch('https://api.example.com/cart');
const data = await response.json();
return data;
},
// create.AsyncThunk 的第二个参数是一个对象
// 在其中根据 API 调用的状态定义减速器。
{
// 当首次调用 API 时运行此项
pending: (state) => {
state.loading = true;
state.error = null;
},
// 在出现错误时运行此项
rejected: (state, action) => {
state.loading = false;
state.error = true;
},
// 成功时运行此项
fulfilled: (state, action) => {
state.loading = false;
state.items = action.payload;
state.total = calculateTotal(state.items); // 定义一个辅助函数
},
},
),
}),
});
添加选择器到你的切片
大多数情况下,你并不想要来自一个切片的整个状态对象。事实上,我不确定为什么你会想要整个对象。这就是为什么你需要选择器,它们是简单的函数,接受一个 Redux 状态作为参数,并返回从该状态派生的数据。
在 Redux Toolkit v2.0 及更高版本中,你可以直接将选择器添加到你的切片中。下面是方法:
//...上面代码中的导入、类型和初始状态
const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
//... 上面代码中的标准减速器
},
selectors: {
selectItems: state => state.items,
selectTotal: state => state.total,
},
});
// 导出选择器
const { selectItems, selectTotal } = cartSlice.selectors;
然后我们可以将这些选择器导入到应用程序的其他部分中:
import { selectItems, selectTotal } from 'store/reducers/cartSlice';
const itemsInCart = selectItems();
使用中间件
Redux Toolkit 提供了一个 getDefaultMiddleware
函数,该函数返回由 configureStore
应用的中间件数组。这个默认中间件集包括:
actionCreatorCheck
:确保使用createAction
创建调度的动作以保持一致性immutableCheck
:警告状态对象中的突变thunk
:允许在操作中启用异步操作和副作用,允许操作内的函数调度其他操作或与外部 API 交互serializableCheck
:如果在状态中检测到不可序列化的值,则发出警告
你可以通过将 middleware
选项传递给 configureStore
来自定义中间件,该选项使用回调函数接收默认中间件作为参数。这是一个例子:
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
// 我们想要添加的中间件
import logger from 'redux-logger';
const store = configureStore({
reducer: rootReducer,
// 使用回调函数来自定义中间件
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
// 你可以禁用或配置默认中间件
.configure({ serializableCheck: false })
// 你可以添加更多中间件
.concat(logger),
});
export default store;
在 Redux Toolkit v2.0 及更高版本中,这个中间件可以是动态的,这意味着你可以在运行时添加它。下面是你如何做到这一点:
import { configureStore, getDefaultMiddleware, createDynamicMiddleware } from '@reduxjs/toolkit';
export const dynamicMiddleware = createDynamicMiddleware();
const store = configureStore({
reducer: rootReducer,
// 使用回调函数来自定义中间件
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
.prepend(dynamicMiddleware.middleware),
});
将上述代码添加到你的存储中会设置存储以接受动态中间件。现在在我们应用程序的其他地方,我们可以在运行时添加中间件,如下所示:
import { dynamicMiddleware } from 'store';
import logger from 'redux-logger';
if (someCondition) {
dynamicMiddleware.addMiddleware(logger);
}
使用 Redux DevTools 进行调试
我不会说 Redux DevTools 扩展,无论是 Chrome 还是 Firefox 版本,在使用 Redux 进行开发时是必需的。但它非常接近 —— 通过 console.log
语句调试你的应用程序的 Redux 状态是可行的,但我会推荐使用 Redux DevTools 而不是 console.log
方法。
好消息是,当你使用 configureStore
时,它会自动为你设置 Redux DevTools。当使用 createStore
时,你必须自己配置 Redux 使用这个扩展。
Redux Toolkit 的使用案例
Redux Toolkit 真的非常灵活,可以用于各种应用。让我们谈谈一些它的亮点。
管理复杂应用状态
Redux 最大的好处是在不断增长的复杂应用中一致地管理状态。Redux Toolkit 让使用 Redux 来做这个目的更简单。它非常适合于:
- 处理来自多个 API 的数据
- 使用 RTK Query(与 RTK 一起提供)进行数据缓存/预取
- 在解耦的组件之间进行通信
- 撤销/重做功能
- 在平台间共享状态逻辑
实现高级状态工作流程
Redux Toolkit 提供了用于复杂工作流程的辅助工具。像 createAsyncThunk
和 createEntityAdaptor
这样的功能将加速实现:
- 异步数据获取
- 实时更新
- 乐观的 UI 更新
一个电商应用可以使用这些功能,在添加商品后乐观地更新购物车数量,而不必等待 API 响应。
迁移现有的 Redux 代码库
如果你已经在使用 Redux,那么现在是时候转向 Redux Toolkit 了,因为它现在是使用 Redux 的官方方式。迁移过程相对简单。你可以在逐渐迁移到 RTK 时继续使用纯粹的 Redux。
Redux Toolkit 的新构建过程
Redux Toolkit 过去存在一些问题,导致它与现代 JavaScript 特性不兼容,包括:
- ESM 不兼容性:由于
package.json
文件中缺少exports
字段,RTK 无法在客户端和服务器代码中同时正确加载 - 不正确的
.mjs
导入:在.mjs
文件中导入 RTK 失败,因为使用了module
但没有exports
字段 - TypeScript
node16
模块解析:RTK 与新的 TypeScript 模块解析选项不兼容
Redux 团队采取了行动来解决这些限制,在 v2.0 中做了以下更改:
package.json
中的exports
字段:现代 ESM 构建现在是主要的输出,而 CJS 用于兼容性。这定义了要加载的构建文件,并确保在不同环境中正确使用- 构建输出现代化:不再进行转译 —— RTK 现在以现代 ES2020 JavaScript 语法为目标,与当前的 JavaScript 标准一致。此外,构建文件被整合到
./dist/
下,简化了包结构。 TypeScript 支持也得到了增强,最低支持的版本是 v4.7 - 放弃 UMD 构建:由于今天的使用案例有限,删除了 UMD 构建,主要用于直接脚本标签导入。一个适用于浏览器的 ESM 构建 ——
dist/$PACKAGE_NAME.browser.mjs
—— 仍然可用于通过 Unpkg 加载脚本标签