✨ 1 RTK 概述与核心优势
Redux Toolkit(RTK)是 Redux 官方推荐的高效开发工具集,旨在简化 Redux 的配置与开发流程。它通过一组经过深思熟虑且“有意见”的 API,解决了 Redux 学习曲线陡峭、配置复杂以及书写大量样板代码的问题。
1.1 为什么需要 RTK?
传统 Redux 开发中,你通常需要手动配置 store、编写大量的 action types、action creators 和 reducers,并且为了处理异步逻辑,还需要额外添加中间件如 redux-thunk。RTK 通过提供一系列工具,内置了最佳实践,极大地提升了开发体验和代码质量。
1.2 RTK 的核心优势
- 简化配置:
configureStore提供了开箱即用的默认配置,包括集成 Redux DevTools 和默认包含redux-thunk中间件。 - 减少样板代码:
createSlice自动生成 action types、action creators 和 reducers,你只需定义 reducer 函数和初始状态。 - 内置不可变更新逻辑:使用 Immer 库,允许你在 reducers 中编写“可变”逻辑,但实际上生成的是不可变更新。
- 强大的异步处理:
createAsyncThunk简化了异步 action 的处理,RTK Query则提供了强大的数据获取和缓存能力。 - 优秀的 TypeScript 支持:RTK 从一开始就为 TypeScript 设计,提供了良好的类型安全。
🛠️ 2 安装与设置
2.1 安装 RTK 和 React-Redux
在你的 React 项目中,使用 npm 或 yarn 安装必要的包:
npm install @reduxjs/toolkit react-redux
# 或
yarn add @reduxjs/toolkit react-redux
2.2 创建 Redux Store
使用 configureStore 来创建 Redux store。它简化了 store 的配置过程,并设置了一些有用的默认值。
// app/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
// 使用 configureStore 创建 store
export default configureStore({
reducer: {
counter: counterReducer, // 添加 slice reducer
// 可以在此添加其他 slice reducers
},
// middleware, devTools 等选项均有智能默认值,通常无需额外配置
});
configureStore 会自动:
- 组合你的 slice reducers
- 添加
redux-thunk中间件以支持异步操作 - 启用 Redux DevTools Extension 以便调试
2.3 为 React 提供 Redux Store
你需要用 <Provider> 组件包裹你的 React 应用,并将上面创建的 store 作为 prop 传递给它,这样 React 组件才能访问到 Redux store。
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import store from './app/store';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
{/* 提供 store */}
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
🧩 3 核心概念与基本用法
3.1 使用 createSlice 创建 State Slice
createSlice 是 RTK 中一个非常重要的 API,它允许你通过定义一个 slice 来生成 action creators 和 reducer,无需手动编写大量的样板代码。
// features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
// 定义初始状态
const initialState = {
value: 0,
};
// 创建一个 slice
export const counterSlice = createSlice({
name: 'counter', // slice 的名称,用于生成 action types
initialState, // 初始状态
reducers: { // 定义 reducers
// increment reducer
increment: (state) => {
// 这里看起来像是直接修改了 state,但由于 Immer 的存在,实际上是安全的不可变更新
state.value += 1;
},
// decrement reducer
decrement: (state) => {
state.value -= 1;
},
// incrementByAmount reducer,接受一个参数
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
// 自动生成的 action creators
// 每个 reducer 函数都会生成一个对应的 action creator
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// 导出 slice 的 reducer
export default counterSlice.reducer;
关键点说明:
name:slice 的名称,用于生成 action type 的前缀(例如counter/increment)。initialState:该 slice 的初始状态值。reducers:一个对象,其中每个键都会生成一个 action creator 函数和一个 reducer case。RTK 使用 Immer 库,允许你以“可变”的方式编写更新逻辑,但最终会生成不可变更新。- 生成的 Action Creators:
createSlice会自动为每个 reducer 函数生成一个对应的 action creator(例如increment()返回{ type: 'counter/increment' })。 - Slice Reducer:需要将其导出并添加到 store 的
reducer配置中。
3.2 在 React 组件中使用 State 和 Actions
在 React 组件中,你可以使用 react-redux 提供的 useSelector 和 useDispatch 钩子来与 Redux store 交互。
// features/counter/Counter.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { decrement, increment, incrementByAmount } from './counterSlice';
import styles from './Counter.module.css';
export function Counter() {
// useSelector 从 store 的 state 中提取数据
// 这里获取的是 state.counter.value(因为我们在 configureStore 中将 counterReducer 放在了 `counter` 键下)
const count = useSelector((state) => state.counter.value);
// useDispatch 返回 store 的 dispatch 方法,用于分发 actions
const dispatch = useDispatch();
return (
<div>
<div>
<button
aria-label="Increment value"
onClick={() => dispatch(increment())} // 分发 increment action
>
Increment
</button>
<span>{count}</span> {/* 显示计数器的值 */}
<button
aria-label="Decrement value"
onClick={() => dispatch(decrement())} // 分发 decrement action
>
Decrement
</button>
<button
onClick={() => dispatch(incrementByAmount(5))} // 分发带有 payload 的 action
>
Increment by 5
</button>
</div>
</div>
);
}
关键点说明:
useSelector:这是一个钩子,允许你从 Redux store 状态树中提取所需的任何数据。它接受一个选择器函数,其参数是整个 store state,返回的是你需要的部分状态。useDispatch:这个钩子返回一个对 store 的dispatch方法的引用,你可以用它来分发 actions。- 分发 Actions:通过调用
dispatch(actionCreator(...args))来改变 store 中的状态。组件通常会在用户交互(如点击按钮)时分发 actions。
⚡ 4 异步逻辑与数据获取
处理异步操作(如 API 调用)是前端应用中的常见需求。RTK 提供了 createAsyncThunk 和 RTK Query 来简化这些操作。
4.1 使用 createAsyncThunk 处理异步 Actions
createAsyncThunk 用于创建异步 thunk action creator,它会根据给定的 action type 和一个返回 Promise 的函数,自动生成 pending, fulfilled, rejected 三种 action types,并分发相应的 actions。
// features/posts/postsSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
// 创建一个异步 thunk 来获取帖子列表
export const fetchPosts = createAsyncThunk(
'posts/fetchPosts', // action type 前缀
async () => {
const response = await axios.get('https://api.example.com/posts');
return response.data; // 此值将作为 action payload 传递给 fulfilled action
}
);
const postsSlice = createSlice({
name: 'posts',
initialState: {
items: [],
loading: 'idle', // 或 'pending', 'succeeded', 'failed'
error: null
},
reducers: {
// 同步 reducers (可选)
},
// 使用 extraReducers 来处理由 createAsyncThunk 或其他其他地方定义的 actions
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => {
state.loading = 'pending';
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.loading = 'succeeded';
state.items = action.payload; // 将获取到的数据存入 state
})
.addCase(fetchPosts.rejected, (state, action) => {
state.loading = 'failed';
state.error = action.error.message;
});
}
});
export default postsSlice.reducer;
在组件中使用异步 thunk:
// features/posts/PostsList.js
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchPosts } from './postsSlice';
export function PostsList() {
const dispatch = useDispatch();
const posts = useSelector((state) => state.posts.items);
const postStatus = useSelector((state) => state.posts.loading);
const error = useSelector((state) => state.posts.error);
useEffect(() => {
if (postStatus === 'idle') {
dispatch(fetchPosts()); // 组件挂载后获取帖子列表
}
}, [postStatus, dispatch]);
let content;
if (postStatus === 'pending') {
content = <div>Loading...</div>;
} else if (postStatus === 'succeeded') {
content = posts.map(post => (
<div key={post.id}>{post.title}</div>
));
} else if (postStatus === 'failed') {
content = <div>{error}</div>;
}
return (
<section>
<h2>Posts</h2>
{content}
</section>
);
}
4.2 使用 RTK Query 进行数据获取和缓存
RTK Query 是 RTK 中包含的一个强大的数据获取和缓存工具。它专为 Redux 设计,可以简化网络请求、缓存管理、加载状态显示等常见需求,无需编写大量的 thunks 和 reducers。
// api/apiSlice.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
// 使用 createApi 创建 API slice
export const apiSlice = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }), // 基础请求 URL
tagTypes: ['Post'], // 定义标签类型用于缓存失效和重新获取
endpoints: (builder) => ({
// 定义查询端点
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Post'], // 此查询提供 'Post' 标签
}),
// 定义变更端点(如创建、更新、删除)
addNewPost: builder.mutation({
query: (initialPost) => ({
url: '/posts',
method: 'POST',
body: initialPost,
}),
invalidatesTags: ['Post'], // 此变更使 'Post' 标签失效,触发相关查询重新获取
}),
}),
});
// 自动生成的 React hooks
export const { useGetPostsQuery, useAddNewPostMutation } = apiSlice;
在组件中使用 RTK Query:
// features/posts/PostsListWithRTKQ.js
import React from 'react';
import { useGetPostsQuery, useAddNewPostMutation } from '../api/apiSlice';
export function PostsListWithRTKQ() {
// 使用自动生成的 hook 进行查询
const {
data: posts,
isLoading,
isSuccess,
isError,
error
} = useGetPostsQuery(); // 缓存、加载状态等均由 RTK Query 自动管理
// 使用自动生成的 hook 进行变更
const [addNewPost, { isLoading: isAdding }] = useAddNewPostMutation();
let content;
if (isLoading) {
content = <div>Loading...</div>;
} else if (isSuccess) {
content = posts.map(post => (
<div key={post.id}>{post.title}</div>
));
} else if (isError) {
content = <div>{error.toString()}</div>;
}
return (
<section>
<h2>Posts (with RTK Query)</h2>
<button onClick={() => addNewPost({ title: 'New Post', content: '...' })}>
Add Post
</button>
{content}
</section>
);
}
RTK Query 的优势:
- 自动化缓存:自动缓存数据,避免重复请求。
- 生命周期管理:自动管理加载状态、错误状态。
- 数据失效和重新获取:通过标签系统轻松实现数据同步。
- 优化性能:内置了对重复请求的去重和查询结果的优化选择。
- 减少代码量:无需手动编写异步 thunks、reducers 来处理加载状态和结果。
🧠 5 最佳实践与项目结构
5.1 项目结构建议
一个良好的项目结构有助于代码维护和团队协作。对于使用 RTK 的 React 项目,可以考虑按功能(feature)组织:
src/
├── app/
│ ├── store.js # Store 配置
│ └── hooks.js # 可选的,用于导出类型化的 hooks(如果使用 TypeScript)
├── features/
│ ├── counter/ # Counter 功能模块
│ │ ├── counterSlice.js
│ │ └── Counter.js
│ └── posts/ # Posts 功能模块
│ ├── postsSlice.js
│ └── PostsList.js
├── api/ # RTK Query API slice(s)
│ └── apiSlice.js
└── index.js
5.2 性能优化
- 记忆化 Selectors:使用
reselect库的createSelector创建记忆化的 selectors,避免不必要的重计算和组件重渲染。RTK 从@reduxjs/toolkit导出了createSelector。 - 规范化状态:对于嵌套或关系型数据,考虑使用规范化结构来避免重复和数据不一致。RTK 提供了
createEntityAdapter来帮助管理规范化数据。
5.3 调试
- Redux DevTools:RTK 的
configureStore默认启用了 Redux DevTools Extension。你可以利用它来查看 action 日志、状态快照、进行“时间旅行”调试等。
📊 传统 Redux 与 RTK 写法对比
下面的表格对比了传统 Redux 和 RTK 在一些常见操作上的写法差异,帮助你更直观地理解 RTK 带来的简化:
| 功能 | 传统 Redux | Redux Toolkit (RTK) | RTK 的优势说明 |
|---|---|---|---|
| 创建 Store | 手动调用 createStore,需自行组合 reducers,手动添加中间件(如 thunk、DevTools)。 | 使用 configureStore,自动组合 reducers,默认包含 thunk 和 DevTools。 | 开箱即用,减少样板代码,默认包含常用工具。 |
| 定义 Actions | 需定义 action types 常量,并编写 action creator 函数。 | 使用 createSlice,自动生成 action types 和 action creators。 | 极大减少样板代码,避免手动编写和维护 action 相关代码。 |
| 编写 Reducer | 通常使用 switch 语句,需手动处理不可变更新,逻辑复杂易出错。 | 使用 createSlice 的 reducers,支持"可变"语法(Immer 支持),自动生成不可变更新。 | 简化 reducer 逻辑,更直观的代码,减少不可变更新错误。 |
| 异步操作 | 需自行安装和配置中间件(如 redux-thunk),手动分发 pending/fulfilled/rejected 等 actions。 | 使用 createAsyncThunk,自动生成处理异步生命周期的 actions;或使用 RTK Query。 | 内置异步处理,自动化异步状态管理;RTK Query 极大简化数据获取和缓存。 |
| TypeScript | 需大量手动定义类型。 | 一流的 TS 支持,API 设计充分考虑类型推断,减少类型定义工作。 | 更好的开发体验和类型安全。 |
💡 提示:RTK 的核心在于简化和约定。它提供了开箱即用的 sensible defaults(合理的默认值)和强大的工具,让你能更高效地编写 Redux 代码。建议你遵循其官方推荐的模式和最佳实践。
💎 总结
Redux Toolkit 通过提供一系列强大的 API(如 configureStore, createSlice, createAsyncThunk, RTK Query),极大地改善了 Redux 的开发体验。它减少了样板代码,内置了最佳实践(如 Immer 用于不可变更新、默认包含常用中间件),并提供了出色的 TypeScript 支持。