服务端阅读 05月28日 01:41
在 React 项目中如何正确使用 axios?
在 React 项目中使用 axios,核心挑战不在于发送请求本身,而在于如何让请求逻辑与 React 的组件生命周期、状态管理、性能优化正确配合。许多开发者会写 axios 调用,却在内存泄漏、竞态条件、重复请求等问题上频频踩坑。一、为什么 React 项目需要封装 axios直接在每个组件中 import axios from 'axios' 然后调用,看似简单,但会带来三个问题: baseURL 和超时配置散落各处、token 刷新逻辑无法统一处理、错误处理方式不一致。创建统一的请求实例// services/request.jsimport axios from 'axios';const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000, headers: { 'Content-Type': 'application/json' },});// 请求拦截器:注入 tokenrequest.interceptors.request.use((config) => { const token = localStorage.getItem('access_token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config;});// 响应拦截器:统一错误处理 + token 过期刷新let isRefreshing = false;let pendingRequests = [];request.interceptors.response.use( (response) => response.data, async (error) => { const originalRequest = error.config; if (error.response?.status === 401 && !originalRequest._retry) { if (isRefreshing) { return new Promise((resolve) => { pendingRequests.push((token) => { originalRequest.headers.Authorization = `Bearer ${token}`; resolve(request(originalRequest)); }); }); } originalRequest._retry = true; isRefreshing = true; try { const { data } = await axios.post('/auth/refresh', { refresh_token: localStorage.getItem('refresh_token'), }); localStorage.setItem('access_token', data.access_token); pendingRequests.forEach((cb) => cb(data.access_token)); pendingRequests = []; return request(originalRequest); } catch { localStorage.clear(); window.location.href = '/login'; return Promise.reject(error); } finally { isRefreshing = false; } } return Promise.reject(error); });export default request;这个封装解决了一个容易被忽略的问题:当多个请求同时返回 401 时,只触发一次 token 刷新,其余请求排队等待新 token 后自动重试。按业务模块拆分 API 函数// services/user.jsimport request from './request';export const userApi = { getProfile: () => request.get('/user/profile'), updateProfile: (data) => request.put('/user/profile', data),};// services/post.jsexport const postApi = { getList: (params) => request.get('/posts', { params }), getDetail: (id) => request.get(`/posts/${id}`), create: (data) => request.post('/posts', data),};将 API 函数与组件解耦,后续接口变更只需改一处。测试时也可以直接 mock 整个模块。二、在组件中正确使用 axios必须处理请求取消React 组件卸载后,仍在进行中的异步请求如果试图更新状态,会触发内存泄漏警告。这是 axios 在 React 中最常见的坑。import { useEffect, useState } from 'react';import { userApi } from '@/services/user';function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const controller = new AbortController(); (async () => { try { setLoading(true); const data = await userApi.getProfile({ signal: controller.signal, }); setUser(data); } catch (err) { if (err.name !== 'CanceledError') { setError(err.message); } } finally { setLoading(false); } })(); return () => controller.abort(); }, [userId]); // ...渲染逻辑}关键点:AbortController 的 signal 传给 axios 的请求配置,组件卸载时调用 controller.abort(),axios 会以 CanceledError 拒绝 Promise。在 catch 中通过 err.name 过滤掉取消错误,避免污染错误状态。竞态条件:快速切换时的数据错乱当用户快速切换 tab 或搜索关键词时,多个请求可能乱序返回,导致页面显示的是旧数据而非最新请求的结果。// 错误写法:快速切换 userId 时可能显示旧数据useEffect(() => { userApi.getProfile(userId).then(setUser);}, [userId]);// 正确写法:每次新请求自动取消上一次useEffect(() => { const controller = new AbortController(); userApi.getProfile(userId, { signal: controller.signal }) .then(setUser) .catch((err) => { if (err.name !== 'CanceledError') setError(err); }); return () => controller.abort();}, [userId]);同一个 AbortController 同时解决了内存泄漏和竞态两个问题。三、用自定义 Hook 收敛请求逻辑每个组件都写一遍 loading/error 状态和 AbortController,代码重复且容易遗漏。封装成自定义 Hook 后,组件只需关注业务逻辑。// hooks/useRequest.jsimport { useState, useEffect, useCallback, useRef } from 'react';export function useRequest(apiFn, { immediate = true, deps = [] } = {}) { const [data, setData] = useState(null); const [loading, setLoading] = useState(immediate); const [error, setError] = useState(null); const controllerRef = useRef(null); const execute = useCallback(async (...args) => { controllerRef.current?.abort(); const controller = new AbortController(); controllerRef.current = controller; try { setLoading(true); setError(null); const result = await apiFn(...args, { signal: controller.signal }); setData(result); return result; } catch (err) { if (err.name !== 'CanceledError') { setError(err); throw err; } } finally { setLoading(false); } }, [apiFn]); useEffect(() => { if (immediate) execute(); return () => controllerRef.current?.abort(); }, [immediate, execute, ...deps]); return { data, loading, error, execute };}这个 Hook 的设计要点:用 useRef 保存最新的 controller 引用,execute 每次调用先取消上一次请求,天然防竞态;immediate 控制是否自动执行;deps 支持依赖变化时重新请求。四、四个常见坑点坑点 1:表单重复提交用户连续点击提交按钮,会发出多个相同的 POST 请求。const [submitting, setSubmitting] = useState(false);const handleSubmit = async (values) => { if (submitting) return; setSubmitting(true); try { await postApi.create(values); } finally { setSubmitting(false); }};// JSX 中<Button loading={submitting} onClick={() => handleSubmit(formValues)}> 提交</Button>更彻底的方案是用 axios 的 CancelToken 或 AbortController 取消上一次提交,但大多数场景下 loading 锁定已足够。坑点 2:POST 请求参数序列化axios 默认将对象序列化为 JSON,但某些后端接口要求 application/x-www-form-urlencoded 格式。直接传对象会导致后端解析失败。// 错误:后端收不到参数axios.post('/api/login', { username: 'admin', password: '123' });// 正确:使用 URLSearchParams 或 qs 库axios.post('/api/login', new URLSearchParams({ username: 'admin', password: '123' }));// 或使用 qsimport qs from 'qs';axios.post('/api/login', qs.stringify({ username: 'admin', password: '123' }));坑点 3:文件上传进度丢失上传大文件时用户需要看到进度,但很多人不知道 axios 支持 onUploadProgress。const [progress, setProgress] = useState(0);const handleUpload = async (file) => { const formData = new FormData(); formData.append('file', file); await request.post('/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (e) => { setProgress(Math.round((e.loaded * 100) / e.total)); }, });};坑点 4:错误处理不区分业务错误和网络错误// 统一错误处理策略request.interceptors.response.use( (response) => { const { code, data, message } = response.data; if (code !== 0) { // 业务错误:弹提示,不抛异常 showToast(message); return Promise.reject(new Error(message)); } return data; }, (error) => { // 网络错误 / 服务器错误 if (!error.response) { showToast('网络异常,请检查网络连接'); } else if (error.response.status >= 500) { showToast('服务器繁忙,请稍后重试'); } return Promise.reject(error); });业务错误(如"余额不足")和网络错误(如断网、500)应该用不同策略处理:前者通常只需提示用户,后者可能需要重试或降级。五、React Query + axios:生产级方案手动管理请求状态、缓存、重试、乐观更新,代码量会急剧膨胀。React Query 把这些能力内置了,只需要提供 axios 请求函数即可。import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';import { postApi } from '@/services/post';// 查询:自动缓存 + 后台刷新 + 请求去重function usePostList(params) { return useQuery({ queryKey: ['posts', params], queryFn: () => postApi.getList(params), staleTime: 5 * 60 * 1000, });}// 变更:自动失效缓存function useCreatePost() { const queryClient = useQueryClient(); return useMutation({ mutationFn: postApi.create, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['posts'] }); }, });}React Query 配合 axios 的核心优势:多个组件请求同一接口时只发一次请求(请求去重);窗口重新获得焦点时自动后台刷新数据;mutation 成功后自动让相关缓存失效,无需手动刷新。TypeScript 类型安全在 TypeScript 项目中,给 axios 请求加上类型约束,能在编译期捕获参数和返回值类型错误。// types/api.tsinterface UserProfile { id: number; name: string; email: string;}interface ApiResponse<T> { code: number; data: T; message: string;}// services/user.tsexport const userApi = { getProfile: () => request.get<ApiResponse<UserProfile>>('/user/profile'),};组件中 useQuery 配合类型推导,data 自动获得 UserProfile 类型,不再需要手动断言。面试中回答 axios 相关问题时,先讲封装思路(实例、拦截器、模块拆分),再讲 React 集成要点(AbortController 防内存泄漏和竞态),最后提 React Query 的缓存和重试机制,这条线能把问题讲透。