在 React 项目中如何正确使用 axios?请说明最佳实践和常见坑点
在 React 项目中使用 axios 时,需要考虑组件生命周期、状态管理、性能优化等多个方面。1. 基础封装创建 API 服务层// api/client.jsimport axios from 'axios';const apiClient = axios.create({ baseURL: process.env.REACT_APP_API_URL, timeout: 10000, headers: { 'Content-Type': 'application/json' }});// 请求拦截器apiClient.interceptors.request.use( (config) => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error));// 响应拦截器apiClient.interceptors.response.use( (response) => response.data, (error) => { if (error.response?.status === 401) { localStorage.removeItem('token'); window.location.href = '/login'; } return Promise.reject(error); });export default apiClient;按模块组织 API// api/userApi.jsimport apiClient from './client';export const userApi = { getProfile: () => apiClient.get('/users/profile'), updateProfile: (data) => apiClient.put('/users/profile', data), uploadAvatar: (formData) => apiClient.post('/users/avatar', formData, { headers: { 'Content-Type': 'multipart/form-data' } })};// api/postApi.jsexport const postApi = { getList: (params) => apiClient.get('/posts', { params }), getDetail: (id) => apiClient.get(`/posts/${id}`), create: (data) => apiClient.post('/posts', data), update: (id, data) => apiClient.put(`/posts/${id}`, data), delete: (id) => apiClient.delete(`/posts/${id}`)};2. 在组件中使用使用 useEffect 和 AbortControllerimport { useEffect, useState } from 'react';import { userApi } from '../api/userApi';function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const controller = new AbortController(); const fetchUser = async () => { try { setLoading(true); const data = await userApi.getProfile(userId, { signal: controller.signal }); setUser(data); } catch (err) { if (!axios.isCancel(err)) { setError(err.message); } } finally { setLoading(false); } }; fetchUser(); // 清理函数:组件卸载时取消请求 return () => { controller.abort(); }; }, [userId]); if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error}</div>; return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> );}自定义 Hook 封装// hooks/useApi.jsimport { useState, useEffect, useCallback } from 'react';import axios from 'axios';export const useApi = (apiFunction, dependencies = []) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const execute = useCallback(async (...params) => { const controller = new AbortController(); try { setLoading(true); setError(null); const result = await apiFunction(...params, { signal: controller.signal }); setData(result); return result; } catch (err) { if (!axios.isCancel(err)) { setError(err); throw err; } } finally { setLoading(false); } return () => controller.abort(); }, dependencies); return { data, loading, error, execute };};// 使用function PostList() { const { data: posts, loading, error, execute: fetchPosts } = useApi(postApi.getList); useEffect(() => { fetchPosts(); }, [fetchPosts]); if (loading) return <Spinner />; if (error) return <ErrorMessage error={error} />; return ( <ul> {posts?.map(post => <PostItem key={post.id} post={post} />)} </ul> );}3. 常见坑点及解决方案坑点 1:内存泄漏警告// ❌ 错误示例:组件卸载后设置状态useEffect(() => { fetchUser().then(data => { setUser(data); // 组件卸载后可能报错 });}, []);// ✅ 正确做法:使用 AbortController 或标记位useEffect(() => { let isMounted = true; const controller = new AbortController(); fetchUser({ signal: controller.signal }) .then(data => { if (isMounted) { setUser(data); } }); return () => { isMounted = false; controller.abort(); };}, []);坑点 2:竞态条件// ❌ 错误示例:快速切换时显示旧数据useEffect(() => { fetchUser(userId).then(data => { setUser(data); // 可能显示之前请求的结果 });}, [userId]);// ✅ 正确做法:取消之前的请求useEffect(() => { const controller = new AbortController(); fetchUser(userId, { signal: controller.signal }) .then(data => setUser(data)) .catch(err => { if (!axios.isCancel(err)) { setError(err); } }); return () => controller.abort();}, [userId]);坑点 3:表单重复提交// ❌ 错误示例:重复点击提交const handleSubmit = async (values) => { await createPost(values); // 可以重复点击};// ✅ 正确做法:使用 loading 状态防止重复提交const [submitting, setSubmitting] = useState(false);const handleSubmit = async (values) => { if (submitting) return; setSubmitting(true); try { await createPost(values); message.success('创建成功'); } catch (error) { message.error(error.message); } finally { setSubmitting(false); }};坑点 4:错误边界处理// ErrorBoundary.jsimport React from 'react';class ApiErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error) { return { hasError: true, error }; } componentDidCatch(error, errorInfo) { console.error('API Error:', error, errorInfo); } render() { if (this.state.hasError) { return <ErrorFallback error={this.state.error} />; } return this.props.children; }}4. 与状态管理结合使用 Context + useReducer// contexts/ApiContext.jsconst ApiContext = createContext();const initialState = { users: [], loading: false, error: null};function apiReducer(state, action) { switch (action.type) { case 'FETCH_START': return { ...state, loading: true, error: null }; case 'FETCH_SUCCESS': return { ...state, loading: false, users: action.payload }; case 'FETCH_ERROR': return { ...state, loading: false, error: action.payload }; default: return state; }}export function ApiProvider({ children }) { const [state, dispatch] = useReducer(apiReducer, initialState); const fetchUsers = useCallback(async () => { dispatch({ type: 'FETCH_START' }); try { const users = await userApi.getList(); dispatch({ type: 'FETCH_SUCCESS', payload: users }); } catch (error) { dispatch({ type: 'FETCH_ERROR', payload: error.message }); } }, []); return ( <ApiContext.Provider value={{ ...state, fetchUsers }}> {children} </ApiContext.Provider> );}使用 React Query(推荐)import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';// 获取数据function useUsers() { return useQuery({ queryKey: ['users'], queryFn: () => userApi.getList(), staleTime: 5 * 60 * 1000, // 5分钟缓存 });}// 修改数据function useCreateUser() { const queryClient = useQueryClient(); return useMutation({ mutationFn: userApi.create, onSuccess: () => { // 成功后刷新用户列表 queryClient.invalidateQueries({ queryKey: ['users'] }); } });}// 组件中使用function UserManager() { const { data: users, isLoading } = useUsers(); const createUser = useCreateUser(); const handleCreate = async (values) => { await createUser.mutateAsync(values); }; if (isLoading) return <Spinner />; return ( <div> <UserList users={users} /> <CreateUserForm onSubmit={handleCreate} /> </div> );}5. 性能优化请求去重// hooks/useDedupedApi.jsconst pendingRequests = new Map();export const useDedupedApi = (apiFunction) => { return useCallback(async (...params) => { const key = JSON.stringify({ func: apiFunction.name, params }); if (pendingRequests.has(key)) { return pendingRequests.get(key); } const promise = apiFunction(...params).finally(() => { pendingRequests.delete(key); }); pendingRequests.set(key, promise); return promise; }, [apiFunction]);};乐观更新const useOptimisticUpdate = () => { const queryClient = useQueryClient(); const updateOptimistically = useCallback(async ({ queryKey, mutationFn, updateFn, rollbackOnError = true }) => { // 取消正在进行的重新获取 await queryClient.cancelQueries({ queryKey }); // 保存之前的状态 const previousData = queryClient.getQueryData(queryKey); // 乐观更新 queryClient.setQueryData(queryKey, updateFn); try { await mutationFn(); } catch (error) { // 出错时回滚 if (rollbackOnError) { queryClient.setQueryData(queryKey, previousData); } throw error; } }, [queryClient]); return { updateOptimistically };};最佳实践总结封装 API 层:统一处理配置、拦截器、错误处理使用 AbortController:组件卸载时取消请求,防止内存泄漏自定义 Hooks:复用请求逻辑,统一管理 loading/error 状态防止重复提交:使用 loading 状态或防抖处理考虑使用 React Query:自动处理缓存、重试、乐观更新等错误边界:使用 ErrorBoundary 捕获渲染错误竞态处理:确保只显示最新请求的结果