Redux Toolkit 深入
现代 Redux:基础使用、Slice/Thunk/RTK Query、核心原理与源码解析
概述
Redux Toolkit(RTK) 是 Redux 官方推荐的工具集,解决了原生 Redux "样板代码太多"的痛点。它内置了 Immer(可变风格更新)、createAsyncThunk(异步)、RTK Query(数据获取)等能力,是大型团队和复杂应用的首选。
一、优势与劣势
优势
- 官方标准:Redux 团队维护,文档完善,社区庞大,大量企业级项目验证
- 严格规范:单向数据流 + action + reducer,团队协作时代码风格统一
- 内置 Immer:在 reducer 中直接修改 state(
state.items.push(...)),自动生成不可变更新 - DevTools 最强:Redux DevTools 支持 action 日志、时间旅行调试、状态快照导入导出
- RTK Query:内置的数据获取/缓存方案,类似 TanStack Query,与 Redux store 深度集成
- createAsyncThunk:标准化异步流程(pending/fulfilled/rejected 三种状态)
- 中间件体系:可扩展的中间件管道(logger、analytics、WebSocket 等)
- TypeScript 优秀:完整的类型推断,
configureStore自动推断 RootState
劣势
- 体积最大:gzip 后约 11KB(对比 Zustand ~1KB)
- 样板代码仍不少:虽然比原生 Redux 少很多,但 slice + thunk + type 定义仍有一定模板
- 必须 Provider:根组件必须包裹
<Provider store={store}> - 学习曲线:需要理解 action、reducer、dispatch、middleware、thunk 等概念
- 过度架构:小型项目使用 RTK 是过度工程化
二、基础使用
安装
bash
npm install @reduxjs/toolkit react-redux创建 Slice
jsx
import { createSlice } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'todos',
initialState: {
items: [],
filter: 'all', // 'all' | 'active' | 'done'
},
reducers: {
addTodo: (state, action) => {
// 直接 push!RTK 内置 Immer
state.items.push({
id: Date.now(),
text: action.payload,
done: false,
});
},
toggleTodo: (state, action) => {
const todo = state.items.find(t => t.id === action.payload);
if (todo) todo.done = !todo.done; // 直接修改
},
removeTodo: (state, action) => {
state.items = state.items.filter(t => t.id !== action.payload);
},
setFilter: (state, action) => {
state.filter = action.payload;
},
},
});
// 自动生成 action creators
export const { addTodo, toggleTodo, removeTodo, setFilter } = todosSlice.actions;
export default todosSlice.reducer;异步 Thunk
jsx
import { createAsyncThunk } from '@reduxjs/toolkit';
// 创建异步 thunk
export const fetchTodos = createAsyncThunk(
'todos/fetch', // action type 前缀
async (_, { rejectWithValue }) => {
try {
const res = await fetch('/api/todos');
if (!res.ok) throw new Error('请求失败');
return await res.json();
} catch (err) {
return rejectWithValue(err.message);
}
}
);
// 在 slice 中处理异步状态
const todosSlice = createSlice({
name: 'todos',
initialState: { items: [], loading: false, error: null },
reducers: { /* ... */ },
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
})
.addCase(fetchTodos.rejected, (state, action) => {
state.loading = false;
state.error = action.payload ?? action.error.message;
});
},
});创建 Store
typescript
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from './todosSlice';
import userReducer from './userSlice';
const store = configureStore({
reducer: {
todos: todosReducer,
user: userReducer,
},
// middleware 默认包含 thunk + serializableCheck + immutableCheck
});
// 类型导出
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// 类型安全的 Hooks
import { useSelector, useDispatch } from 'react-redux';
export const useAppSelector = useSelector.withTypes<RootState>();
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();在组件中使用
jsx
import { Provider } from 'react-redux';
import { useAppSelector, useAppDispatch } from './store';
import { addTodo, toggleTodo, removeTodo, fetchTodos } from './todosSlice';
// 根组件包裹 Provider
function App() {
return (
<Provider store={store}>
<TodoApp />
</Provider>
);
}
function TodoApp() {
const { items, loading, error } = useAppSelector((state) => state.todos);
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(fetchTodos());
}, [dispatch]);
if (loading) return <Spinner />;
if (error) return <ErrorMessage message={error} />;
return (
<div>
<button onClick={() => dispatch(addTodo('新任务'))}>添加</button>
<ul>
{items.map(todo => (
<li key={todo.id}>
<span
onClick={() => dispatch(toggleTodo(todo.id))}
style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
>
{todo.text}
</span>
<button onClick={() => dispatch(removeTodo(todo.id))}>删除</button>
</li>
))}
</ul>
</div>
);
}三、RTK Query
RTK Query 是 Redux Toolkit 内置的数据获取与缓存方案,类似 TanStack Query 但与 Redux store 深度集成。
typescript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Post', 'User'],
endpoints: (builder) => ({
// 查询
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Post'],
}),
getPostById: builder.query({
query: (id) => `/posts/${id}`,
providesTags: (result, error, id) => [{ type: 'Post', id }],
}),
// 变更
createPost: builder.mutation({
query: (body) => ({ url: '/posts', method: 'POST', body }),
invalidatesTags: ['Post'], // 创建后自动重新获取列表
}),
deletePost: builder.mutation({
query: (id) => ({ url: `/posts/${id}`, method: 'DELETE' }),
invalidatesTags: (result, error, id) => [{ type: 'Post', id }],
}),
}),
});
export const {
useGetPostsQuery,
useGetPostByIdQuery,
useCreatePostMutation,
useDeletePostMutation,
} = api;
// 添加到 store
const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
// ...其他 reducer
},
middleware: (getDefault) => getDefault().concat(api.middleware),
});jsx
// 组件中使用
function PostList() {
const { data: posts, isLoading, error } = useGetPostsQuery();
const [deletePost] = useDeletePostMutation();
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage />;
return (
<ul>
{posts.map(post => (
<li key={post.id}>
{post.title}
<button onClick={() => deletePost(post.id)}>删除</button>
</li>
))}
</ul>
);
}四、Selector 与 createSelector
typescript
import { createSelector } from '@reduxjs/toolkit';
// 基础 selector
const selectTodos = (state: RootState) => state.todos.items;
const selectFilter = (state: RootState) => state.todos.filter;
// 记忆化 selector(只有输入变化时才重新计算)
const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'active': return todos.filter(t => !t.done);
case 'done': return todos.filter(t => t.done);
default: return todos;
}
}
);
// 使用
function FilteredList() {
const filteredTodos = useAppSelector(selectFilteredTodos);
// filter 或 todos 不变时,selectFilteredTodos 返回缓存结果
}五、核心原理
Redux 数据流
用户操作 → dispatch(action) → middleware 管道 → reducer → 新 state → 通知订阅者 → 重渲染
action = { type: 'todos/addTodo', payload: '学习 Redux' }
↓
middleware[0] → middleware[1] → ... → reducer
(thunk) (logger) (纯函数)
↓
newState = { todos: [...] }
↓
store.getState() 返回新 state
↓
useSelector 比较 → 触发重渲染createSlice 原理
javascript
// createSlice 做了什么?
function createSlice({ name, initialState, reducers }) {
const actionCreators = {};
const caseReducers = {};
for (const [key, reducer] of Object.entries(reducers)) {
const type = `${name}/${key}`; // 如 'todos/addTodo'
actionCreators[key] = (payload) => ({ type, payload });
caseReducers[type] = reducer;
}
// 最终的 reducer(包裹 Immer produce)
const reducer = produce((draft, action) => {
const caseReducer = caseReducers[action.type];
if (caseReducer) {
return caseReducer(draft, action);
}
}, initialState);
return { actions: actionCreators, reducer };
}Immer 如何工作
javascript
import { produce } from 'immer';
// produce 的简化原理:
// 1. 创建 state 的 Proxy 代理(draft)
// 2. 在 draft 上记录所有修改操作
// 3. 根据修改记录生成新的不可变对象
const nextState = produce(currentState, (draft) => {
draft.todos.push({ text: '新任务' }); // 看起来是修改
});
// 实际:currentState 未变,nextState 是新对象
// nextState !== currentState
// nextState.todos !== currentState.todos
// 但未修改的部分共享引用:nextState.user === currentState.useruseSelector 的重渲染机制
javascript
function useSelector(selector) {
const store = useContext(ReduxContext);
return useSyncExternalStore(
store.subscribe,
() => selector(store.getState())
);
// store 变化 → 调用 selector → 与上次结果比较(===)
// 相同 → 跳过渲染
// 不同 → 触发渲染
}六、最佳实践
项目结构(Feature-based)
src/
├── features/
│ ├── todos/
│ │ ├── todosSlice.ts # slice + thunk + selector
│ │ ├── TodoList.tsx # 组件
│ │ └── todosApi.ts # RTK Query endpoints
│ └── user/
│ ├── userSlice.ts
│ └── UserProfile.tsx
├── app/
│ ├── store.ts # configureStore
│ └── hooks.ts # useAppSelector / useAppDispatch
└── services/
└── api.ts # createApi base面试高频题
Q: Redux Toolkit 比原生 Redux 好在哪?
- createSlice 合并了 action type + action creator + reducer,减少 60%+ 样板代码
- 内置 Immer:reducer 中直接"修改" state,无需手写展开运算符
- configureStore:自动配置 DevTools、thunk 中间件、序列化检查
- createAsyncThunk:标准化异步流程,自动 dispatch pending/fulfilled/rejected
- RTK Query:内置数据获取缓存,替代 saga/thunk 中手写的 API 逻辑
Q: Redux 中间件是什么?工作原理?
中间件是 dispatch → reducer 之间的拦截层,形成管道。每个中间件接收 (store) => (next) => (action) => { ... } 的柯里化函数。可以拦截、修改、延迟 action,也可以发起副作用。常见的 thunk 中间件就是检测 action 是否是函数,是则执行它。
Q: 什么时候选 Redux Toolkit 而不是 Zustand?
- 大型团队需要严格规范和统一的架构模式
- 需要强大的 DevTools(时间旅行、action 日志)
- 已有 Redux 代码需要渐进式升级
- 需要 RTK Query 替代手写的 API 层
- 需要中间件管道处理复杂副作用(如 WebSocket、日志分析)