下面,我们来系统的梳理关于 Redux Toolkit 异步操作:createAsyncThunk 的基本知识点:
一、createAsyncThunk 概述
1.1 为什么需要 createAsyncThunk
在 Redux 中处理异步操作(如 API 调用)时,传统方法需要手动处理:
- 多个 action(请求开始、成功、失败)
- 复杂的 reducer 逻辑
- 错误处理重复代码
- 取消操作难以实现
createAsyncThunk 解决的问题:
- 自动生成异步生命周期 actions
- 简化异步状态管理(pending/fulfilled/rejected)
- 内置错误处理机制
- 支持请求取消
1.2 核心特点
- 标准化流程:自动生成三种 action 类型
- Promise 集成:基于 Promise 的异步操作
- 错误处理:自动捕获错误并 dispatch rejected action
- TypeScript 友好:完整的类型支持
- Redux Toolkit 集成:与 createSlice 无缝协作
二、基本用法与核心概念
2.1 创建异步 Thunk
import { createAsyncThunk } from '@reduxjs/toolkit';export const fetchUser = createAsyncThunk(// 唯一标识符:'feature/actionName''users/fetchUser',// 异步 payload 创建器async (userId, thunkAPI) => {try {const response = await fetch(`/api/users/${userId}`);return await response.json(); // 作为 fulfilled action 的 payload} catch (error) {// 返回拒绝原因return thunkAPI.rejectWithValue(error.message);}}
);
2.2 参数详解
参数 | 类型 | 说明 |
---|---|---|
typePrefix | string | 唯一标识符,自动生成三种 action 类型 |
payloadCreator | function | 包含异步逻辑的函数,返回 Promise |
options | object | 可选配置项(如条件执行) |
2.3 自动生成的 Action Types
fetchUser.pending; // 'users/fetchUser/pending'
fetchUser.fulfilled; // 'users/fetchUser/fulfilled'
fetchUser.rejected; // 'users/fetchUser/rejected'
三、与 createSlice 集成
3.1 在 extraReducers 中处理状态
import { createSlice } from '@reduxjs/toolkit';
import { fetchUser } from './userThunks';const userSlice = createSlice({name: 'user',initialState: {data: null,status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'error: null},reducers: {// 同步 reducers...},extraReducers: (builder) => {builder.addCase(fetchUser.pending, (state) => {state.status = 'loading';state.error = null;}).addCase(fetchUser.fulfilled, (state, action) => {state.status = 'succeeded';state.data = action.payload;}).addCase(fetchUser.rejected, (state, action) => {state.status = 'failed';state.error = action.payload || action.error.message;});}
});export default userSlice.reducer;
3.2 状态管理最佳实践
const initialState = {data: null,// 异步状态标识isLoading: false,isSuccess: false,isError: false,error: null
};// 在 extraReducers 中:
.addCase(fetchUser.pending, (state) => {state.isLoading = true;
})
.addCase(fetchUser.fulfilled, (state, action) => {state.isLoading = false;state.isSuccess = true;state.data = action.payload;
})
.addCase(fetchUser.rejected, (state, action) => {state.isLoading = false;state.isError = true;state.error = action.payload;
});
四、高级功能与技巧
4.1 访问 State 和 Dispatch
通过 thunkAPI
参数访问:
export const updateUser = createAsyncThunk('users/updateUser',async (userData, thunkAPI) => {const { getState, dispatch } = thunkAPI;// 获取当前状态const { auth } = getState();const token = auth.token;try {const response = await fetch('/api/users', {method: 'PUT',headers: {'Authorization': `Bearer ${token}`,'Content-Type': 'application/json'},body: JSON.stringify(userData)});if (!response.ok) {// 处理 API 错误const error = await response.json();throw new Error(error.message);}// 触发其他 actiondispatch(showNotification('用户信息已更新'));return await response.json();} catch (error) {return thunkAPI.rejectWithValue(error.message);}}
);
4.2 条件执行(Conditional Execution)
export const fetchUser = createAsyncThunk('users/fetchUser',async (userId, thunkAPI) => {// 实现逻辑...},{condition: (userId, { getState }) => {const { users } = getState();// 如果用户已在缓存中,则取消请求if (users.data[userId]) {return false; // 取消执行}// 如果正在加载,则取消if (users.status === 'loading') {return false;}return true; // 允许执行}}
);
4.3 请求取消
export const searchProducts = createAsyncThunk('products/search',async (query, thunkAPI) => {// 创建取消令牌const controller = new AbortController();const signal = controller.signal;// 注册取消回调thunkAPI.signal.addEventListener('abort', () => {controller.abort();});try {const response = await fetch(`/api/products?q=${query}`, { signal });return await response.json();} catch (error) {if (error.name === 'AbortError') {// 请求被取消,不视为错误return thunkAPI.rejectWithValue({ aborted: true });}return thunkAPI.rejectWithValue(error.message);}}
);// 在组件中取消请求
useEffect(() => {const promise = dispatch(searchProducts(query));return () => {promise.abort(); // 组件卸载时取消请求};
}, [dispatch, query]);
4.4 乐观更新
export const updatePost = createAsyncThunk('posts/update',async (postData, thunkAPI) => {const { id, ...data } = postData;const response = await api.updatePost(id, data);return response.data;}
);// 在 createSlice 中
extraReducers: (builder) => {builder.addCase(updatePost.fulfilled, (state, action) => {const index = state.posts.findIndex(p => p.id === action.payload.id);if (index !== -1) {state.posts[index] = action.payload;}}).addCase(updatePost.rejected, (state, action) => {// 回滚乐观更新const originalPost = action.meta.arg.originalPost;const index = state.posts.findIndex(p => p.id === originalPost.id);if (index !== -1) {state.posts[index] = originalPost;}});
}// 在 dispatch 时传递原始数据
dispatch(updatePost({id: 123,title: '新标题',originalPost: currentPost // 保存原始数据用于回滚
}));
五、错误处理
5.1 统一错误格式
export const fetchData = createAsyncThunk('data/fetch',async (_, thunkAPI) => {try {const response = await api.getData();return response.data;} catch (error) {// 标准化错误格式return thunkAPI.rejectWithValue({code: error.response?.status || 500,message: error.message,details: error.response?.data?.errors});}}
);// 在 reducer 中
.addCase(fetchData.rejected, (state, action) => {state.error = {code: action.payload.code || 500,message: action.payload.message || '未知错误',details: action.payload.details};
});
5.2 全局错误处理
// 中间件:全局错误处理
const errorLoggerMiddleware = store => next => action => {if (action.type.endsWith('/rejected')) {const error = action.error || action.payload;console.error('Redux 异步错误:', {type: action.type,error: error.message || error,stack: error.stack});// 发送错误到监控服务trackError(error);}return next(action);
};// 配置 store
const store = configureStore({reducer: rootReducer,middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(errorLoggerMiddleware)
});
六、测试策略
6.1 测试异步 Thunk
import configureStore from '@reduxjs/toolkit';
import { fetchUser } from './userThunks';
import userReducer from './userSlice';describe('fetchUser async thunk', () => {let store;beforeEach(() => {store = configureStore({reducer: {user: userReducer}});// 模拟 fetch APIglobal.fetch = jest.fn();});it('处理成功的用户获取', async () => {const mockUser = { id: 1, name: 'John' };fetch.mockResolvedValue({ok: true,json: () => Promise.resolve(mockUser)});await store.dispatch(fetchUser(1));const state = store.getState().user;expect(state.data).toEqual(mockUser);expect(state.status).toBe('succeeded');});it('处理失败的用户获取', async () => {fetch.mockRejectedValue(new Error('Network error'));await store.dispatch(fetchUser(1));const state = store.getState().user;expect(state.error).toBe('Network error');expect(state.status).toBe('failed');});
});
6.2 测试 Slice 的 extraReducers
import userReducer, { fetchUserPending, fetchUserFulfilled, fetchUserRejected
} from './userSlice';describe('userSlice extraReducers', () => {const initialState = {data: null,status: 'idle',error: null};it('应处理 fetchUser.pending', () => {const action = { type: fetchUser.pending.type };const state = userReducer(initialState, action);expect(state).toEqual({data: null,status: 'loading',error: null});});it('应处理 fetchUser.fulfilled', () => {const mockUser = { id: 1, name: 'John' };const action = { type: fetchUser.fulfilled.type,payload: mockUser};const state = userReducer(initialState, action);expect(state).toEqual({data: mockUser,status: 'succeeded',error: null});});it('应处理 fetchUser.rejected', () => {const error = 'Failed to fetch';const action = { type: fetchUser.rejected.type,payload: error};const state = userReducer(initialState, action);expect(state).toEqual({data: null,status: 'failed',error});});
});
七、实践与性能优化
7.1 组织代码结构
src/├── app/│ └── store.js├── features/│ └── users/│ ├── usersSlice.js│ ├── userThunks.js // 异步 thunks│ ├── userSelectors.js│ └── UserList.js└── services/└── api.js // API 客户端
7.2 创建 API 服务层
// services/api.js
import axios from 'axios';const api = axios.create({baseURL: '/api',timeout: 10000,headers: {'Content-Type': 'application/json'}
});export const fetchUser = (userId) => api.get(`/users/${userId}`);
export const createUser = (userData) => api.post('/users', userData);
export const updateUser = (userId, userData) => api.put(`/users/${userId}`, userData);
export const deleteUser = (userId) => api.delete(`/users/${userId}`);export default api;
7.3 封装可复用 Thunk 逻辑
// utils/createThunk.js
export function createThunk(typePrefix, apiCall) {return createAsyncThunk(typePrefix,async (arg, thunkAPI) => {try {const response = await apiCall(arg);return response.data;} catch (error) {const message = error.response?.data?.message || error.message;return thunkAPI.rejectWithValue(message);}});
}// 使用示例
import { createThunk } from '../utils/createThunk';
import { fetchUser } from '../../services/api';export const getUser = createThunk('users/getUser', fetchUser);
八、案例:电商应用商品管理
8.1 商品 Thunks
// features/products/productThunks.js
import { createAsyncThunk } from '@reduxjs/toolkit';
import { fetchProducts, fetchProductDetails,createProduct,updateProduct,deleteProduct
} from '../../services/api';export const loadProducts = createAsyncThunk('products/load',async (category, thunkAPI) => {try {const response = await fetchProducts(category);return response.data;} catch (error) {return thunkAPI.rejectWithValue(error.message);}}
);export const loadProductDetails = createAsyncThunk('products/loadDetails',async (productId, thunkAPI) => {try {const response = await fetchProductDetails(productId);return response.data;} catch (error) {return thunkAPI.rejectWithValue(error.message);}},{condition: (productId, { getState }) => {const { products } = getState();// 避免重复加载return !products.details[productId];}}
);export const addNewProduct = createAsyncThunk('products/add',async (productData, thunkAPI) => {try {const response = await createProduct(productData);return response.data;} catch (error) {return thunkAPI.rejectWithValue(error.response.data.errors);}}
);
8.2 商品 Slice
// features/products/productsSlice.js
import { createSlice } from '@reduxjs/toolkit';
import { loadProducts, loadProductDetails,addNewProduct
} from './productThunks';const initialState = {items: [],details: {},status: 'idle',loadingDetails: {},error: null,createStatus: 'idle'
};const productsSlice = createSlice({name: 'products',initialState,reducers: {clearProductError: (state) => {state.error = null;}},extraReducers: (builder) => {builder// 加载商品列表.addCase(loadProducts.pending, (state) => {state.status = 'loading';state.error = null;}).addCase(loadProducts.fulfilled, (state, action) => {state.status = 'succeeded';state.items = action.payload;}).addCase(loadProducts.rejected, (state, action) => {state.status = 'failed';state.error = action.payload;})// 加载商品详情.addCase(loadProductDetails.pending, (state, action) => {state.loadingDetails[action.meta.arg] = true;}).addCase(loadProductDetails.fulfilled, (state, action) => {state.loadingDetails[action.meta.arg] = false;state.details[action.meta.arg] = action.payload;}).addCase(loadProductDetails.rejected, (state, action) => {state.loadingDetails[action.meta.arg] = false;// 可以单独存储每个商品的错误信息})// 创建新商品.addCase(addNewProduct.pending, (state) => {state.createStatus = 'loading';state.error = null;}).addCase(addNewProduct.fulfilled, (state, action) => {state.createStatus = 'succeeded';state.items.unshift(action.payload); // 乐观更新}).addCase(addNewProduct.rejected, (state, action) => {state.createStatus = 'failed';state.error = action.payload;});}
});export const { clearProductError } = productsSlice.actions;
export default productsSlice.reducer;
九、总结
9.1 createAsyncThunk 核心优势
- 简化异步流程:自动生成三种 action 类型
- 标准化状态管理:pending/fulfilled/rejected 生命周期
- 内置错误处理:rejectWithValue 标准化错误
- 高级功能支持:条件执行、请求取消、乐观更新
- 测试友好:清晰的异步流程便于测试
9.2 实践总结
- 分离业务逻辑:使用服务层封装 API 调用
- 标准化错误处理:统一错误格式和全局处理
- 合理使用条件执行:避免不必要的请求
- 实施乐观更新:提升用户体验
- 组件卸载时取消请求:避免内存泄漏
- 使用 TypeScript:增强类型安全和开发体验