5月28日 01:41
How to correctly use axios in React projects? Please explain best practices and common pitfalls
When using axios in React projects, you need to consider component lifecycle, state management, performance optimization, and other aspects.
1. Basic Encapsulation
Creating API Service Layer
javascript// api/client.js import axios from 'axios'; const apiClient = axios.create({ baseURL: process.env.REACT_APP_API_URL, timeout: 10000, headers: { 'Content-Type': 'application/json' } }); // Request interceptor apiClient.interceptors.request.use( (config) => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error) ); // Response interceptor 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;
Organizing APIs by Module
javascript// api/userApi.js import 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.js export 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. Using in Components
Using useEffect and AbortController
javascriptimport { 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(); // Cleanup function: cancel request when component unmounts 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> ); }
Custom Hook Encapsulation
javascript// hooks/useApi.js import { 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 }; }; // Usage 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. Common Pitfalls and Solutions
Pitfall 1: Memory Leak Warning
javascript// ❌ Wrong example: setting state after component unmounts useEffect(() => { fetchUser().then(data => { setUser(data); // May error after component unmounts }); }, []); // ✅ Correct approach: use AbortController or flag useEffect(() => { let isMounted = true; const controller = new AbortController(); fetchUser({ signal: controller.signal }) .then(data => { if (isMounted) { setUser(data); } }); return () => { isMounted = false; controller.abort(); }; }, []);
Pitfall 2: Race Conditions
javascript// ❌ Wrong example: displaying old data when switching quickly useEffect(() => { fetchUser(userId).then(data => { setUser(data); // May display results from previous request }); }, [userId]); // ✅ Correct approach: cancel previous requests 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]);
Pitfall 3: Form Duplicate Submission
javascript// ❌ Wrong example: repeated click submission const handleSubmit = async (values) => { await createPost(values); // Can be clicked repeatedly }; // ✅ Correct approach: use loading state to prevent duplicate submission const [submitting, setSubmitting] = useState(false); const handleSubmit = async (values) => { if (submitting) return; setSubmitting(true); try { await createPost(values); message.success('Created successfully'); } catch (error) { message.error(error.message); } finally { setSubmitting(false); } };
Pitfall 4: Error Boundary Handling
javascript// ErrorBoundary.js import 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. Integration with State Management
Using Context + useReducer
javascript// contexts/ApiContext.js const 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> ); }
Using React Query (Recommended)
javascriptimport { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; // Fetch data function useUsers() { return useQuery({ queryKey: ['users'], queryFn: () => userApi.getList(), staleTime: 5 * 60 * 1000, // 5 minute cache }); } // Modify data function useCreateUser() { const queryClient = useQueryClient(); return useMutation({ mutationFn: userApi.create, onSuccess: () => { // Refresh user list after success queryClient.invalidateQueries({ queryKey: ['users'] }); } }); } // Usage in component 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. Performance Optimization
Request Deduplication
javascript// hooks/useDedupedApi.js const 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]); };
Optimistic Updates
javascriptconst useOptimisticUpdate = () => { const queryClient = useQueryClient(); const updateOptimistically = useCallback(async ({ queryKey, mutationFn, updateFn, rollbackOnError = true }) => { // Cancel ongoing refetches await queryClient.cancelQueries({ queryKey }); // Save previous state const previousData = queryClient.getQueryData(queryKey); // Optimistic update queryClient.setQueryData(queryKey, updateFn); try { await mutationFn(); } catch (error) { // Rollback on error if (rollbackOnError) { queryClient.setQueryData(queryKey, previousData); } throw error; } }, [queryClient]); return { updateOptimistically }; };
Best Practices Summary
- Encapsulate API Layer: Unified handling of configuration, interceptors, error handling
- Use AbortController: Cancel requests when component unmounts to prevent memory leaks
- Custom Hooks: Reuse request logic,统一管理 loading/error states
- Prevent Duplicate Submission: Use loading state or debounce handling
- Consider Using React Query: Automatically handles caching, retries, optimistic updates
- Error Boundaries: Use ErrorBoundary to catch rendering errors
- Race Condition Handling: Ensure only the latest request results are displayed