Introduction to Axios
Axios is a promise-based HTTP client for the browser and Node.js that makes it easy to send asynchronous HTTP requests and handle responses. It has become the de facto standard for making API requests in React applications due to its simplicity, powerful features, and cross-browser compatibility.
Key Features of Axios
- Make XMLHttpRequests from the browser
- Make HTTP requests from Node.js
- Supports the Promise API
- Intercept request and response
- Transform request and response data
- Automatic transforms for JSON data
- Client-side protection against XSRF
- Cancel requests
- Automatic handling of request timeouts
Axios vs. Fetch API
| Feature | Axios | Fetch API |
|---|---|---|
| Browser Support | All browsers via polyfills | Modern browsers only |
| Response Timeout | Yes, built-in | Requires manual implementation |
| Request Cancellation | Yes, built-in | Requires AbortController |
| Automatic JSON Parsing | Yes | Requires .json() call |
| Error Handling | HTTP errors trigger catch blocks | HTTP error responses still resolve |
| Interceptors | Yes, built-in | Requires manual implementation |
| Request/Response Transform | Yes, built-in | Requires manual implementation |
| Download Progress | Yes | No built-in support |
Real-world analogy: If HTTP requests were like sending mail, Fetch would be like basic postal service (gets the job done with minimal features), while Axios would be like a premium courier service with tracking, insurance, and special handling options.
Getting Started with Axios
Installation
Install Axios in your React project using npm or yarn:
npm install axios
# or
yarn add axios
Basic Usage
Here's a simple example of how to use Axios for different types of HTTP requests:
import axios from 'axios';
// GET request
const fetchUsers = async () => {
try {
const response = await axios.get('https://api.example.com/users');
console.log(response.data);
return response.data;
} catch (error) {
console.error('Error fetching users:', error);
throw error;
}
};
// POST request
const createUser = async (userData) => {
try {
const response = await axios.post('https://api.example.com/users', userData);
console.log('User created:', response.data);
return response.data;
} catch (error) {
console.error('Error creating user:', error);
throw error;
}
};
// PUT request
const updateUser = async (userId, userData) => {
try {
const response = await axios.put(`https://api.example.com/users/${userId}`, userData);
console.log('User updated:', response.data);
return response.data;
} catch (error) {
console.error('Error updating user:', error);
throw error;
}
};
// DELETE request
const deleteUser = async (userId) => {
try {
const response = await axios.delete(`https://api.example.com/users/${userId}`);
console.log('User deleted:', response.data);
return response.data;
} catch (error) {
console.error('Error deleting user:', error);
throw error;
}
};
Axios Response Object
When an Axios request is successful, it returns a response object with the following properties:
{
// `data` is the response that was provided by the server
data: {},
// `status` is the HTTP status code from the server response
status: 200,
// `statusText` is the HTTP status message from the server response
statusText: 'OK',
// `headers` the HTTP headers that the server responded with
headers: {},
// `config` is the config that was provided to `axios` for the request
config: {},
// `request` is the request that generated this response
request: {}
}
Error Handling
When an Axios request fails, it returns an error object that contains information about the error:
axios.get('/api/data')
.then(response => {
// Handle success
console.log(response.data);
})
.catch(error => {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.error('Error data:', error.response.data);
console.error('Error status:', error.response.status);
console.error('Error headers:', error.response.headers);
} else if (error.request) {
// The request was made but no response was received
console.error('No response received:', error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.error('Error message:', error.message);
}
console.error('Error config:', error.config);
});
Creating an Axios Instance
Instead of using Axios methods directly, it's a best practice to create a custom Axios instance with predefined configurations. This approach makes your code more maintainable and allows for consistent handling of requests across your application.
// services/api.js
import axios from 'axios';
// Create an instance with custom configs
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
export default api;
Using environment variables for different environments:
// services/api.js
import axios from 'axios';
const baseURL = process.env.REACT_APP_API_URL || 'https://api.example.com';
const api = axios.create({
baseURL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
export default api;
Real-world application: This approach is used in production applications to manage different API endpoints for development, staging, and production environments.
Using the Axios Instance
// services/userService.js
import api from './api';
export const userService = {
// Get all users
getUsers: async () => {
const response = await api.get('/users');
return response.data;
},
// Get user by ID
getUserById: async (id) => {
const response = await api.get(`/users/${id}`);
return response.data;
},
// Create a new user
createUser: async (userData) => {
const response = await api.post('/users', userData);
return response.data;
},
// Update a user
updateUser: async (id, userData) => {
const response = await api.put(`/users/${id}`, userData);
return response.data;
},
// Delete a user
deleteUser: async (id) => {
const response = await api.delete(`/users/${id}`);
return response.data;
}
};
Advantages of this approach:
- Centralized API configuration
- DRY (Don't Repeat Yourself) principle - no duplicate code
- Easy to add global configurations and interceptors
- Simplified testing and mocking
- Easier to change API endpoints or authentication mechanisms
Request and Response Interceptors
Interceptors are one of the most powerful features of Axios. They allow you to intercept requests or responses before
they are handled by then or catch.
Request Interceptors
Request interceptors can be used to add authentication tokens, format request data, or log requests. This is particularly useful for adding JWT tokens to every outgoing request.
// Add a request interceptor
api.interceptors.request.use(
(config) => {
// Do something before request is sent
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error) => {
// Do something with request error
return Promise.reject(error);
}
);
Response Interceptors
Response interceptors can be used to globally process response data, handle errors, or refresh tokens when they expire.
// Add a response interceptor
api.interceptors.response.use(
(response) => {
// Any status code within the range of 2xx causes this function to trigger
// Do something with response data
return response;
},
async (error) => {
const originalRequest = error.config;
// If error is 401 (Unauthorized) and not already retrying
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// Attempt to refresh the token
const refreshToken = localStorage.getItem('refreshToken');
const response = await api.post('/auth/refresh', { refreshToken });
// Store the new tokens
const { token } = response.data;
localStorage.setItem('token', token);
// Update the authorization header
api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
// Retry the original request with the new token
return api(originalRequest);
} catch (refreshError) {
// If refresh fails, redirect to login
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
// Return any other error
return Promise.reject(error);
}
);
Real-world application: This pattern is commonly used in production applications to handle authentication token expiration without disrupting the user experience. When a token expires, the app automatically refreshes it and retries the failed request.
Advanced Axios Features
Concurrent Requests
You can use axios.all() to make multiple requests in parallel and handle all the responses together.
// Make multiple concurrent requests
const fetchDashboardData = async () => {
try {
// Define all requests
const userRequest = api.get('/users/me');
const postsRequest = api.get('/posts');
const notificationsRequest = api.get('/notifications');
// Execute all requests concurrently
const [userResponse, postsResponse, notificationsResponse] = await Promise.all([
userRequest,
postsRequest,
notificationsRequest
]);
// Process all responses together
return {
user: userResponse.data,
posts: postsResponse.data,
notifications: notificationsResponse.data
};
} catch (error) {
console.error('Error fetching dashboard data:', error);
throw error;
}
};
Request Cancellation
Axios supports cancelling requests using the CancelToken API. This is useful for scenarios where you need to cancel an ongoing request, such as when a user navigates away from a page or types in a search field.
const CancelToken = axios.CancelToken;
let cancel;
// Search function with cancellation
const searchUsers = async (query) => {
try {
// Cancel the previous request if it exists
if (cancel) {
cancel('New search request initiated');
}
// Make a new request with a cancel token
const response = await api.get('/users/search', {
params: { q: query },
cancelToken: new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
cancel = c;
})
});
return response.data;
} catch (error) {
if (axios.isCancel(error)) {
// Request was cancelled
console.log('Request cancelled:', error.message);
return [];
}
// Handle other errors
console.error('Search error:', error);
throw error;
}
};
In React components, it's common to use this pattern with the useEffect hook for cleanup:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import api from '../services/api';
function SearchComponent() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
// Skip if query is empty
if (!query.trim()) {
setResults([]);
return;
}
// Create a cancel token
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
const fetchResults = async () => {
setLoading(true);
setError(null);
try {
const response = await api.get('/search', {
params: { q: query },
cancelToken: source.token
});
setResults(response.data);
} catch (err) {
if (axios.isCancel(err)) {
// Request was cancelled, no need to set error
console.log('Request cancelled:', err.message);
} else {
setError('Failed to fetch results');
console.error(err);
}
} finally {
setLoading(false);
}
};
// Debounce the search to avoid making too many requests
const timeoutId = setTimeout(() => {
fetchResults();
}, 500);
// Cleanup function
return () => {
clearTimeout(timeoutId);
source.cancel('Component unmounted or new search initiated');
};
}, [query]);
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
{loading && <div>Loading...</div>}
{error && <div className="error">{error}</div>}
<ul>
{results.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
Timeout Configuration
You can set timeouts for requests to prevent them from hanging indefinitely.
// Global timeout for all requests
const api = axios.create({
baseURL: '/api',
timeout: 10000, // 10 seconds
});
// Timeout for a specific request
api.get('/users', {
timeout: 5000 // 5 seconds
});
File Upload with Progress Tracking
Axios makes it easy to track upload progress for file uploads, which is useful for providing feedback to users.
import React, { useState } from 'react';
import axios from 'axios';
function FileUploader() {
const [file, setFile] = useState(null);
const [progress, setProgress] = useState(0);
const [uploading, setUploading] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
const handleFileChange = (e) => {
if (e.target.files[0]) {
setFile(e.target.files[0]);
setProgress(0);
setError(null);
setSuccess(false);
}
};
const handleUpload = async () => {
if (!file) {
setError('Please select a file');
return;
}
setUploading(true);
setProgress(0);
setError(null);
setSuccess(false);
// Create form data
const formData = new FormData();
formData.append('file', file);
try {
await axios.post('/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
setProgress(percentCompleted);
}
});
setSuccess(true);
} catch (err) {
setError('Upload failed: ' + (err.response?.data?.message || err.message));
} finally {
setUploading(false);
}
};
return (
<div className="file-uploader">
<input
type="file"
onChange={handleFileChange}
disabled={uploading}
/>
{file && (
<div>
<p>Selected file: {file.name}</p>
<button
onClick={handleUpload}
disabled={uploading}
>
{uploading ? 'Uploading...' : 'Upload'}
</button>
</div>
)}
{uploading && (
<div className="progress-bar-container">
<div
className="progress-bar"
style={{ width: `${progress}%` }}
></div>
<span>{progress}%</span>
</div>
)}
{error && <div className="error">{error}</div>}
{success && <div className="success">File uploaded successfully!</div>}
</div>
);
}
Using Axios with React Hooks
Creating custom hooks for API calls can greatly simplify your React components and provide reusable data fetching logic.
useApi Custom Hook
// hooks/useApi.js
import { useState, useEffect, useCallback } from 'react';
import axios from 'axios';
const useApi = (url, options = {}) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [controller, setController] = useState(null);
// Function to fetch data
const fetchData = useCallback(async (body = null) => {
// Cancel previous request
if (controller) {
controller.abort();
}
// Create a new AbortController
const newController = new AbortController();
setController(newController);
setLoading(true);
setError(null);
try {
const response = await axios({
url,
...options,
signal: newController.signal,
data: body
});
setData(response.data);
return response.data;
} catch (err) {
if (axios.isCancel(err)) {
// Request was cancelled
console.log('Request cancelled:', err.message);
} else {
setError(err.response?.data?.message || err.message);
throw err;
}
} finally {
setLoading(false);
}
}, [url, options]);
// Initial data fetch if autoFetch is true
useEffect(() => {
if (options.autoFetch !== false) {
fetchData();
}
// Cleanup: cancel request on unmount
return () => {
if (controller) {
controller.abort();
}
};
}, [fetchData, options.autoFetch]);
return { data, loading, error, fetchData };
};
export default useApi;
Using the Custom Hook
import React from 'react';
import useApi from '../hooks/useApi';
function UserList() {
const { data: users, loading, error, fetchData: refetch } = useApi('/api/users');
const handleRefresh = () => {
refetch();
};
if (loading) {
return <div>Loading users...</div>;
}
if (error) {
return (
<div>
<p>Error: {error}</p>
<button onClick={handleRefresh}>Try Again</button>
</div>
);
}
return (
<div>
<h2>Users</h2>
<button onClick={handleRefresh}>Refresh</button>
{users && users.length > 0 ? (
<ul>
{users.map(user => (
<li key={user.id}>{user.name} ({user.email})</li>
))}
</ul>
) : (
<p>No users found.</p>
)}
</div>
);
}
CRUD Operations Hook
// hooks/useCrud.js
import { useState } from 'react';
import api from '../services/api';
const useCrud = (baseUrl) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// Get all items
const getAll = async () => {
setLoading(true);
setError(null);
try {
const response = await api.get(baseUrl);
setData(response.data);
return response.data;
} catch (err) {
setError(err.response?.data?.message || err.message);
throw err;
} finally {
setLoading(false);
}
};
// Get single item by ID
const getById = async (id) => {
setLoading(true);
setError(null);
try {
const response = await api.get(`${baseUrl}/${id}`);
return response.data;
} catch (err) {
setError(err.response?.data?.message || err.message);
throw err;
} finally {
setLoading(false);
}
};
// Create new item
const create = async (item) => {
setLoading(true);
setError(null);
try {
const response = await api.post(baseUrl, item);
setData(prevData => [...prevData, response.data]);
return response.data;
} catch (err) {
setError(err.response?.data?.message || err.message);
throw err;
} finally {
setLoading(false);
}
};
// Update existing item
const update = async (id, item) => {
setLoading(true);
setError(null);
try {
const response = await api.put(`${baseUrl}/${id}`, item);
setData(prevData =>
prevData.map(d => d.id === id ? response.data : d)
);
return response.data;
} catch (err) {
setError(err.response?.data?.message || err.message);
throw err;
} finally {
setLoading(false);
}
};
// Delete item
const remove = async (id) => {
setLoading(true);
setError(null);
try {
await api.delete(`${baseUrl}/${id}`);
setData(prevData => prevData.filter(d => d.id !== id));
return true;
} catch (err) {
setError(err.response?.data?.message || err.message);
throw err;
} finally {
setLoading(false);
}
};
return {
data,
loading,
error,
getAll,
getById,
create,
update,
remove
};
};
export default useCrud;
Using the CRUD Hook in a Component
import React, { useState, useEffect } from 'react';
import useCrud from '../hooks/useCrud';
function TodoManager() {
const {
data: todos,
loading,
error,
getAll,
create,
update,
remove
} = useCrud('/api/todos');
const [newTodo, setNewTodo] = useState('');
useEffect(() => {
// Load todos when component mounts
getAll();
}, [getAll]);
const handleAddTodo = async (e) => {
e.preventDefault();
if (!newTodo.trim()) return;
try {
await create({ title: newTodo, completed: false });
setNewTodo('');
} catch (err) {
console.error('Failed to add todo:', err);
}
};
const handleToggleComplete = async (id, completed) => {
try {
await update(id, { completed: !completed });
} catch (err) {
console.error('Failed to update todo:', err);
}
};
const handleDelete = async (id) => {
try {
await remove(id);
} catch (err) {
console.error('Failed to delete todo:', err);
}
};
if (loading && !todos.length) {
return <div>Loading todos...</div>;
}
if (error) {
return <div>Error: {error}</div>;
}
return (
<div>
<h2>Todo List</h2>
<form onSubmit={handleAddTodo}>
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Add a new todo"
disabled={loading}
/>
<button type="submit" disabled={loading || !newTodo.trim()}>
{loading ? 'Adding...' : 'Add'}
</button>
</form>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggleComplete(todo.id, todo.completed)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.title}
</span>
<button onClick={() => handleDelete(todo.id)}>Delete</button>
</li>
))}
</ul>
{!todos.length && !loading && (
<p>No todos yet. Add one to get started!</p>
)}
</div>
);
}
Testing Axios API Calls
Testing API calls is crucial for ensuring the reliability of your application. There are several approaches to testing Axios requests:
Mock Axios for Unit Tests
For unit tests, you typically want to mock Axios to avoid making real API calls. The `jest-mock-axios` or `axios-mock-adapter` libraries can help with this.
// Example using axios-mock-adapter
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { userService } from './userService';
describe('User Service', () => {
let mock;
beforeEach(() => {
// Create a new instance of mock adapter
mock = new MockAdapter(axios);
});
afterEach(() => {
// Reset the mock
mock.reset();
});
test('getUsers should return a list of users', async () => {
const users = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
];
// Setup the mock to return the users
mock.onGet('/api/users').reply(200, users);
// Call the service method
const result = await userService.getUsers();
// Assert the result
expect(result).toEqual(users);
});
test('getUsers should handle errors', async () => {
// Setup the mock to return an error
mock.onGet('/api/users').reply(500, { message: 'Server error' });
// Call the service method and expect it to throw
await expect(userService.getUsers()).rejects.toThrow();
});
test('createUser should post user data', async () => {
const newUser = { name: 'New User', email: 'new@example.com' };
const createdUser = { id: 3, ...newUser };
// Setup the mock
mock.onPost('/api/users', newUser).reply(201, createdUser);
// Call the service method
const result = await userService.createUser(newUser);
// Assert the result
expect(result).toEqual(createdUser);
});
});
Testing Hooks with Axios
// Example testing a custom hook with React Testing Library
import { renderHook, act } from '@testing-library/react-hooks';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import useApi from './useApi';
describe('useApi hook', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.reset();
});
test('should fetch data successfully', async () => {
const data = { id: 1, name: 'Test Item' };
mock.onGet('/api/test').reply(200, data);
const { result, waitForNextUpdate } = renderHook(() =>
useApi('/api/test')
);
// Initially loading is true, and data is null
expect(result.current.loading).toBe(true);
expect(result.current.data).toBe(null);
// Wait for the hook to update
await waitForNextUpdate();
// After the update, loading should be false and data should be set
expect(result.current.loading).toBe(false);
expect(result.current.data).toEqual(data);
expect(result.current.error).toBe(null);
});
test('should handle error', async () => {
mock.onGet('/api/test').reply(500, { message: 'Server error' });
const { result, waitForNextUpdate } = renderHook(() =>
useApi('/api/test')
);
await waitForNextUpdate();
expect(result.current.loading).toBe(false);
expect(result.current.data).toBe(null);
expect(result.current.error).toBeTruthy();
});
test('should refetch data when called explicitly', async () => {
const initialData = { id: 1, name: 'Initial' };
const updatedData = { id: 1, name: 'Updated' };
// First reply with initialData
mock.onGet('/api/test').replyOnce(200, initialData);
const { result, waitForNextUpdate } = renderHook(() =>
useApi('/api/test')
);
await waitForNextUpdate();
expect(result.current.data).toEqual(initialData);
// Update mock to return new data
mock.onGet('/api/test').replyOnce(200, updatedData);
// Call fetchData manually
act(() => {
result.current.fetchData();
});
// Wait for the update
await waitForNextUpdate();
expect(result.current.data).toEqual(updatedData);
});
});
Real-world application: These testing patterns are used in production applications to ensure API calls work correctly across the application, especially when refactoring code or upgrading dependencies.
Best Practices for Axios in React Applications
- Create a centralized API service: Define an Axios instance with global configurations and organize API calls into service modules.
- Use interceptors for common operations: Handle authentication, error processing, and logging with interceptors rather than repeating code.
- Separate concerns: Keep API calls separate from UI components using custom hooks or service functions.
- Handle loading and error states: Always account for loading states and error handling in your components.
- Cancel pending requests: Cancel requests when components unmount or when new requests supersede old ones.
- Use environment variables: Configure API endpoints based on the environment (development, staging, production).
- Implement retry logic: For critical operations, implement retry logic with exponential backoff.
- Debounce rapid requests: For search inputs or other fast-changing values, debounce API calls to reduce server load.
Example Project Structure
src/
|-- api/
| |-- axios.js # Axios instance and interceptors
| |-- index.js # Re-exports all services
| |-- userService.js # User-related API calls
| |-- postService.js # Post-related API calls
| `-- ...
|
|-- hooks/
| |-- useApi.js # Generic API hook
| |-- useUser.js # User-specific hook
| |-- usePost.js # Post-specific hook
| `-- ...
|
|-- components/
| |-- users/
| | |-- UserList.js
| | |-- UserDetail.js
| | `-- ...
| |
| |-- posts/
| | |-- PostList.js
| | |-- PostDetail.js
| | `-- ...
| `-- ...
|
|-- App.js
`-- index.js
Practice Activities
Activity 1: Basic CRUD Application
Create a simple CRUD application for managing a list of products:
- Set up an Axios instance with baseURL and appropriate headers
- Create service functions for all CRUD operations
- Build React components to display, add, edit, and delete products
- Implement proper loading and error states
Use json-server as a mock backend to test your application.
Activity 2: Authentication System
Build an authentication system using Axios:
- Implement login and registration forms
- Create Axios interceptors to handle JWT tokens
- Implement token refresh logic when tokens expire
- Add protected routes that require authentication
Activity 3: File Upload with Progress
Create a file upload component:
- Build a file upload form that supports single and multiple files
- Implement progress tracking and display using Axios
- Add validation for file types and sizes
- Create a preview component for uploaded images
Activity 4: API Testing
Write tests for your API service:
- Set up Jest and Axios mock adapter
- Write unit tests for all API service functions
- Test error scenarios and edge cases
- Test any custom hooks that use Axios
Summary
- Axios Basics: Axios is a powerful HTTP client that simplifies API requests in React applications.
- Axios Instance: Creating a custom Axios instance allows for centralized configuration and consistent request handling.
- Interceptors: Request and response interceptors enable global request processing, such as adding authentication tokens and handling errors.
- Advanced Features: Axios supports concurrent requests, request cancellation, progress tracking, and more.
- React Integration: Custom hooks can encapsulate API logic and provide a clean interface for components.
- Testing: Mock Axios for unit tests to ensure your API service functions work correctly without making real API calls.
- Best Practices: Organize your API calls into service modules, handle loading and error states, and cancel pending requests when appropriate.
Mastering Axios is essential for building robust React applications that communicate effectively with backend services. By implementing the patterns and practices covered in this lecture, you'll be able to create maintainable, testable, and user-friendly applications that provide excellent experiences even when dealing with network requests.