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

javascript
import { 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)

javascript
import { 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

javascript
const 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

  1. Encapsulate API Layer: Unified handling of configuration, interceptors, error handling
  2. Use AbortController: Cancel requests when component unmounts to prevent memory leaks
  3. Custom Hooks: Reuse request logic,统一管理 loading/error states
  4. Prevent Duplicate Submission: Use loading state or debounce handling
  5. Consider Using React Query: Automatically handles caching, retries, optimistic updates
  6. Error Boundaries: Use ErrorBoundary to catch rendering errors
  7. Race Condition Handling: Ensure only the latest request results are displayed
标签:JavaScript前端Axios