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