
一句话:Redux Toolkit(RTK)是官方推荐的 Redux 写法——用
configureStore内置常用中间件、用createSlice同时生成 reducer 与 action(并内置 Immer 可直接「改」状态)、用createAsyncThunk/ RTK Query 处理异步。
一、RTK 解决了什么问题
原生 Redux 的三个痛点:
① 配置繁琐
createStore + applyMiddleware(thunk, logger) + composeWithDevTools + ...
② 样板代码多
每个功能要写:action types 常量、action creator、reducer 的 switch
一个「加一个 todo」要写 20 行
③ 不可变更新难写
深嵌套状态要「逐层展开」,容易出错
RTK 的解法:
① configureStore() → 一键配置(默认含 thunk + DevTools + 开发期检查)
② createSlice() → reducer 与 action 一起定义(样板代码减少约 70%)
③ 内置 Immer → 可以直接「改」state(底层自动生成不可变副本)
④ createAsyncThunk() → 异步请求的标准化写法(自动生成三态 action)
⑤ createEntityAdapter() → 实体列表的标准化管理(增删改查 + 选择器)
⑥ RTK Query → 数据请求 + 缓存(替代手写 thunk + 状态管理)
二、configureStore
// app/store.ts
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from '@/features/todos/todosSlice';
import { api } from '@/features/api/apiSlice';
export const store = configureStore({
reducer: {
todos: todosReducer,
[api.reducerPath]: api.reducer, // RTK Query 的 reducer
},
// ⭐ 下面这些是「默认就有」的,通常不用写:
// middleware: (getDefault) => getDefault().concat(api.middleware), // 需要追加时
// devTools: true, // 默认开发环境开启
// preloadedState: undefined, // 用于 SSR 注入状态
});
// ⭐ 从 store 推导出类型(不要手写 RootState 类型)
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// app/hooks.ts —— 类型化的 hooks(推荐做法)
import { useDispatch, useSelector, type TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './store';
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
| 对比 | 原生 Redux | RTK |
|---|---|---|
| 创建 store | createStore(reducer, applyMiddleware(...)) |
configureStore({ reducer }) |
| DevTools | 手动 composeWithDevTools |
默认开启 |
| thunk | 手动安装 | 内置 |
| 序列化检查 | 无 | 默认开启(开发期警告) |
| 不可变检查 | 无 | 默认开启(检测误改状态) |
三、createSlice
// features/todos/todosSlice.ts
import { createSlice, nanoid, type PayloadAction } from '@reduxjs/toolkit';
export interface Todo {
id: string;
text: string;
done: boolean;
}
interface TodosState {
items: Todo[];
filter: 'all' | 'active' | 'done';
}
const initialState: TodosState = { items: [], filter: 'all' };
const todosSlice = createSlice({
name: 'todos', // ⭐ action type 的前缀(如 'todos/add')
initialState,
reducers: {
// ⭐ 直接「改」state(RTK 内置 Immer,自动生成不可变副本)
addTodo: {
reducer(state, action: PayloadAction<Todo>) {
state.items.push(action.payload);
},
// 如果 action 需要「准备参数」,用 prepare
prepare(text: string) {
return { payload: { id: nanoid(), text, done: false } };
},
},
removeTodo(state, action: PayloadAction<string>) {
state.items = state.items.filter((t) => t.id !== action.payload);
},
toggleTodo(state, action: PayloadAction<string>) {
const todo = state.items.find((t) => t.id === action.payload);
if (todo) todo.done = !todo.done; // ✅ 直接改,Immer 处理
},
setFilter(state, action: PayloadAction<TodosState['filter']>) {
state.filter = action.payload;
},
},
});
// ⭐ action creator 自动生成(不需要手写 action types 常量)
export const { addTodo, removeTodo, toggleTodo, setFilter } = todosSlice.actions;
// ⭐ selector 一起定义(便于复用)
export const selectTodos = (state: { todos: TodosState }) => state.todos.items;
export const selectFilter = (state: { todos: TodosState }) => state.todos.filter;
export default todosSlice.reducer;
// 组件里使用
function TodoApp() {
const items = useAppSelector(selectTodos);
const filter = useAppSelector(selectFilter);
const dispatch = useAppDispatch();
return (
<>
<button onClick={() => dispatch(addTodo('新任务'))}>添加</button>
<ul>
{items.map((t) => (
<li key={t.id} onClick={() => dispatch(toggleTodo(t.id))}>
{t.done ? '✅' : '⬜'} {t.text}
</li>
))}
</ul>
</>
);
}
createSlice 的三个关键点:
① name 会成为 action type 的前缀:
todosSlice 里的 addTodo → action type 是 'todos/addTodo'
这样不同 slice 的同名 action 不会冲突
② 可以直接「改」state:这是 Immer 的能力
⚠️ 但必须遵守 Immer 规则:
· 不能「既改 draft 又 return 新值」
· 不能把 draft 存到外部
· 裸 `state = xxx`(重新赋值整个 state)需要 return:
setAll(state, action) { return action.payload; } ← 整体替换要 return
③ prepare 用于「自定义 payload 结构」
让 action creator 的调用更简洁:dispatch(addTodo('文本')) 而不是手动构造对象
四、createAsyncThunk
// features/todos/todosThunks.ts
import { createAsyncThunk } from '@reduxjs/toolkit';
export const fetchTodos = createAsyncThunk<
Todo[], // 返回类型(fulfilled 的 payload)
string, // 参数类型
{ rejectValue: string } // thunkAPI 的类型
>(
'todos/fetch', // ⭐ action type 前缀
async (userId, { rejectWithValue, signal, getState, dispatch }) => {
try {
const res = await api.getTodos(userId, { signal }); // ⭐ 支持取消
return res.data;
} catch (err) {
return rejectWithValue(String(err));
}
}
);
// ⭐ 自动生成三个 action(不需要手写)
// 'todos/fetch/pending'
// 'todos/fetch/fulfilled'
// 'todos/fetch/rejected'
// 在 slice 里处理这三态
const todosSlice = createSlice({
name: 'todos',
initialState: { items: [] as Todo[], status: 'idle' as Status, error: null as string | null },
reducers: { /* ... */ },
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.status = 'success';
state.items = action.payload; // ✅ Immer 可直接赋值
})
.addCase(fetchTodos.rejected, (state, action) => {
state.status = 'error';
state.error = action.payload ?? action.error.message ?? 'unknown';
});
},
});
// 组件里使用
function TodoList() {
const { items, status, error } = useAppSelector((s) => s.todos);
const dispatch = useAppDispatch();
useEffect(() => {
// ⭐ 返回 Promise 的 unwrap 方法可以拿到结果 / 处理错误
dispatch(fetchTodos('me')).unwrap().catch((e) => toast.error(String(e)));
}, [dispatch]);
if (status === 'loading') return <Skeleton />;
if (status === 'error') return <ErrorView message={error ?? ''} />;
return <List items={items} />;
}
thunkAPI 的常用成员 |
作用 |
|---|---|
dispatch |
派发其它 action |
getState |
读取当前状态(用类型化的 RootState) |
signal |
AbortSignal(支持取消请求) |
rejectWithValue(v) |
自定义 rejected 的 payload(默认是 error.message) |
fulfillWithValue(v) |
自定义 fulfilled 的 payload |
extra |
configureStore 里传入的 extraArgument |
五、RTK Query(数据请求的现代方案)
// features/api/apiSlice.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = (getState() as RootState).auth.token;
if (token) headers.set('authorization', `Bearer ${token}`);
return headers;
},
}),
tagTypes: ['Todo', 'User'], // ⭐ 用于「失效」的标签
endpoints: (builder) => ({
// 查询(GET)—— 自动缓存、自动生成 useXxxQuery Hook
getTodos: builder.query<Todo[], string>({
query: (userId) => `todos?userId=${userId}`,
providesTags: (result) =>
result ? [...result.map(({ id }) => ({ type: 'Todo' as const, id })), 'Todo'] : ['Todo'],
}),
getUser: builder.query<User, string>({
query: (id) => `users/${id}`,
providesTags: (_, __, id) => [{ type: 'User', id }],
}),
// 变更(POST/PUT/DELETE)—— 自动生成 useXxxMutation Hook
addTodo: builder.mutation<Todo, Partial<Todo>>({
query: (body) => ({ url: 'todos', method: 'POST', body }),
invalidatesTags: ['Todo'], // ⭐ 自动让相关查询失效并重新请求
}),
deleteTodo: builder.mutation<void, string>({
query: (id) => ({ url: `todos/${id}`, method: 'DELETE' }),
invalidatesTags: (_, __, id) => [{ type: 'Todo', id }],
}),
}),
});
export const {
useGetTodosQuery,
useGetUserQuery,
useAddTodoMutation,
useDeleteTodoMutation,
} = api;
// 组件里使用(不再需要手写 loading / error / 缓存)
function TodoList({ userId }: { userId: string }) {
const { data, isLoading, isError, error, refetch } = useGetTodosQuery(userId, {
pollingInterval: 30000, // 轮询
skip: !userId, // 条件跳过
selectFromResult: ({ data }) => ({ data }), // 只订阅需要的字段
});
const [addTodo, { isLoading: isAdding }] = useAddTodoMutation();
if (isLoading) return <Skeleton />;
if (isError) return <ErrorView error={error} onRetry={refetch} />;
return (
<>
<button onClick={() => addTodo({ text: '新任务' })} disabled={isAdding}>
添加
</button>
<ul>{data?.map((t) => <li key={t.id}>{t.text}</li>)}</ul>
</>
);
}
RTK Query 提供的能力(不用手写):
① 缓存(同一请求参数复用同一份数据,多个组件共享)
② loading / error / data 三态
③ 自动失效与重新请求(invalidateTags / providesTags)
④ 轮询(pollingInterval)与乐观更新(onQueryStarted + updateQueryData)
⑤ 分页与无限滚动(通过 merge 自定义缓存合并策略)
⑥ 请求去重、条件请求(skip)、手动刷新(refetch)
| 方案 | 适合 |
|---|---|
| RTK Query | 已经在用 Redux、想要「一个库解决状态+请求」 |
| TanStack Query | 没用 Redux、或想要更专注的查询库(生态更成熟) |
| 手写 thunk | 逻辑特殊的请求(不适合缓存) |
六、createEntityAdapter(实体列表标准化)
import { createEntityAdapter } from '@reduxjs/toolkit';
const todosAdapter = createEntityAdapter<Todo>({
sortComparer: (a, b) => a.text.localeCompare(b.text), // 可选:排序
});
const initialState = todosAdapter.getInitialState({ status: 'idle' as Status });
// 结构:{ ids: ['1','2'], entities: { '1': {...}, '2': {...} }, status: 'idle' }
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
addOne: todosAdapter.addOne, // ⭐ 直接用内置的增删改
addMany: todosAdapter.addMany,
updateOne: todosAdapter.updateOne,
removeOne: todosAdapter.removeOne,
setAll: todosAdapter.setAll,
},
extraReducers: (builder) => {
builder.addCase(fetchTodos.fulfilled, todosAdapter.setAll);
},
});
// ⭐ 自动生成的选择器
export const {
selectAll: selectAllTodos,
selectById: selectTodoById,
selectIds: selectTodoIds,
selectTotal: selectTodoTotal,
} = todosAdapter.getSelectors((state: RootState) => state.todos);
为什么用 EntityAdapter:
① 用「归一化」结构(ids + entities 字典)而不是数组
→ 按 id 查询是 O(1)(不用遍历数组)
② 更新单条数据时不用「map 整个数组」
③ 内置的选择器自带记忆化(引用稳定)
④ 适合「列表 + 频繁更新单项」的场景
七、目录结构建议
src/
├── app/
│ ├── store.ts configureStore + RootState / AppDispatch 类型
│ └── hooks.ts useAppDispatch / useAppSelector(类型化)
└── features/ 按功能划分(feature-first)
├── api/
│ └── apiSlice.ts RTK Query 的 createApi
├── todos/
│ ├── todosSlice.ts createSlice
│ ├── todosThunks.ts createAsyncThunk
│ ├── todosSelectors.ts 记忆化选择器
│ ├── TodoList.tsx 组件
│ └── TodoList.test.tsx
└── auth/
└── ...
两点说明:
① 官方推荐「按功能组织(feature folder)」而不是「按类型组织」
理由:一个功能的 slice / 组件 / 选择器 / 测试放在一起,改需求时只动一个目录
② 通用组件(Button、Modal)放 components/,与 features/ 平级
八、常见坑
// ① 在 reducer 里「既改又返回」
reducers: {
bad(state, action) {
state.items.push(action.payload);
return { ...state, items: action.payload }; // ❌ Immer 会报错
},
}
// ② 直接重新赋值整个 state 却忘了 return
reducers: {
bad2(state, action) {
state = action.payload; // ❌ 无效(需要 return)
},
good(state, action) {
return action.payload; // ✅
},
}
// ③ 把 Immer 的 draft 存到外部
reducers: {
bad3(state, action) {
someExternalVar = state.items; // ❌ draft 在 produce 后失效
},
}
// ④ 在 reducer 里做副作用
reducers: {
bad4(state, action) {
localStorage.setItem('x', 'y'); // ❌
},
}
// ⑤ 在 action 里放「不可序列化的值」
dispatch(addTodo({ id: nanoid(), createdAt: new Date() })); // ⚠️ devTools 警告
// ✅ 用时间戳(number / ISO 字符串)
// ⑥ 手写 RootState 类型(应该用 ReturnType 推导)
interface RootState { todos: TodosState } // ⚠️ 容易漏
type RootState = ReturnType<typeof store.getState>; // ✅
// ⑦ 在 extraReducers 里用「字符串」而不是 action creator
builder.addCase('todos/fetch/fulfilled', ...) // ⚠️ 丢失类型推导
builder.addCase(fetchTodos.fulfilled, ...) // ✅
// ⑧ 用 RTK Query 还手写 thunk 做同样的请求(重复)
// ⑨ 忘记 invalidatesTags → 变更后列表不更新
// ⑩ 在组件里直接 api.endpoints.getTodos.select(...) 取缓存(应该用生成的 Hook)
// ⑪ createSlice 的 name 重复(不同 slice 用同一个 name → action type 冲突)
// ⑫ 在 RTK Query 的 selectFromResult 里返回新对象(会每次都重渲染)
// ⑫ 的正确写法
const { data } = useGetTodosQuery(userId, {
selectFromResult: ({ data }) => ({ data }), // ⭐ 只取原始引用
});
// ❌ selectFromResult: ({ data }) => ({ todos: data?.filter(...) }) → 每次新数组
九、面试延伸
- 「RTK 相比原生 Redux 有什么改进?」
四点:① 配置简化——configureStore一键搞定 thunk、DevTools、开发期检查(不可变检查、序列化检查);② 样板代码大幅减少——createSlice用「一个对象」同时生成 reducer 与 action creator,不再手写 action types 与 switch;③ 内置 Immer——可以直接「改」状态(底层自动生成不可变副本),解决了深层嵌套更新的痛苦;④ 异步标准化——createAsyncThunk自动生成pending/fulfilled/rejected三态 action,RTK Query直接提供请求缓存。 -
「
createSlice里为什么可以直接修改 state?」
因为 RTK 的createSlice内置了 Immer。Immer 用Proxy拦截你对draft的修改,然后基于修改记录生成一份新的不可变对象——所以「写法是直接改,效果是不可变更新」。这既避免了深层展开的繁琐,又保证了 Redux 依赖的「引用变化」。注意两个限制:① 不能「既改 draft 又return新值」;② 不能把draft保存到外部(它在produce结束后失效)。 -
「
createAsyncThunk是怎么工作的?」
它接收「一个 type 前缀」和一个「返回 Promise 的 payload creator」,然后:① 自动生成三个 action——xxx/pending、xxx/fulfilled、xxx/rejected;② 返回一个 thunk action creator——调用它会先派发pending,然后执行你的异步函数,成功派发fulfilled(payload 是返回值),失败派发rejected(payload 是 error);③ 提供thunkAPI(dispatch、getState、signal、rejectWithValue等)。返回的 action 上还有.unwrap(),让调用方可以await结果或捕获错误。 -
「RTK Query 和 TanStack Query 有什么区别?」
能力上非常接近(都提供缓存、失效、重试、乐观更新、分页),主要差异是定位与集成:① RTK Query 是 Redux Toolkit 的一部分——如果项目已经在用 Redux,它能让「状态管理 + 数据请求」统一到一个生态(缓存也在 store 里,可用 DevTools 观察);② TanStack Query 是独立的查询库——不依赖 Redux,生态更成熟(插件、DevTools、SSR 支持都很完善),适合「不想引入 Redux」的项目。选哪个主要看项目是否已有 Redux。 -
「
createEntityAdapter有什么用?」
它把「实体列表」标准化为归一化结构({ ids: [...], entities: { id: entity } }),并提供内置的增删改 reducer 与自带记忆化的选择器。好处是:① 按 id 查询是 O(1)(不用遍历数组);② 更新单项不用「map 整个数组」;③ 选择器返回的引用稳定(避免无谓重渲染);④ 代码量大幅减少。适合「列表 + 频繁更新单项」的场景(如聊天消息、表格行编辑)。 -
「RTK 的错误处理怎么做?」
三种方式:① 在createAsyncThunk里用rejectWithValue(value)——自定义rejectedaction 的 payload(默认只有error.message,不够结构化);② 在extraReducers的rejected分支里更新错误状态(state.error = action.payload);③ 在组件里用.unwrap()捕获——dispatch(fetchX()).unwrap().catch(e => ...),适合「需要就地提示」的场景(如 toast)。此外可以用middleware做「全局错误上报」。 -
「为什么推荐用
useAppSelector/useAppDispatch?」
因为它们是类型化的封装:useAppSelector: TypedUseSelectorHook<RootState>让state参数自动有完整类型(不需要每次手写(state: RootState) => ...),useAppDispatch: () => AppDispatch让dispatch支持 thunk(否则 TS 只认识「普通对象 action」,派发 thunk 会报类型错误)。这是官方推荐做法,能显著减少类型样板代码与错误。 -
「RTK 里怎么做乐观更新?」
两种路径:① 手写——在extraReducers里处理xxx/pending时先「本地改」状态,在rejected时回滚(需要保存旧值);② 用 RTK Query 的onQueryStarted——在 mutation 里updateQueryData立即修改缓存,然后在catch里patch.undo()回滚。RTK Query 的方式更简洁,也自带「查询失效后自动重新拉取」的能力。
一句话速记
RTK = 官方推荐写法:
configureStore(内置 thunk + DevTools + 不可变/序列化检查)、createSlice(reducer 与 action 一起生成,内置 Immer 可直接改 state,命名以name为前缀)、createAsyncThunk(自动生成pending/fulfilled/rejected三态,支持rejectWithValue与.unwrap())、RTK Query(请求缓存 +providesTags/invalidatesTags自动失效 + 轮询 + 乐观更新)、createEntityAdapter(归一化列表 + 自带记忆化选择器);类型用ReturnType<typeof store.getState>推导,用useAppSelector/useAppDispatch避免样板;Immer 的两条禁令是「不能既改又 return」「不能外传 draft」。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!