下面,我們來系統的梳理關于 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:增強類型安全和開發體驗