Using Axios with React State and useEffect
Integrating Axios with React involves triggering asynchronous HTTP
requests within lifecycle hooks like useEffect and storing
the resulting data in component or global state. This article covers how
to execute API calls with Axios inside useEffect, manage
loading and error states with useState, cancel pending
requests during cleanup, and structure the data flow for both local
components and global state managers.
Basic Integration:
Axios inside useEffect
Because React components render synchronously, asynchronous calls
cannot be made directly on the component function itself. Instead, Axios
calls are wrapped in an asynchronous function declared inside the
useEffect hook.
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUsers = async () => {
try {
setLoading(true);
const response = await axios.get('https://jsonplaceholder.typicode.com/users');
setUsers(response.data);
} catch (err) {
setError(err.message || 'An error occurred');
} finally {
setLoading(false);
}
};
fetchUsers();
}, []); // Empty dependency array runs the effect once on mount
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
export default UserList;Key Components of the Pattern:
- Three State Variables: Managing
data,loading, anderrorensures predictable UI rendering across all request phases. - Internal Async Function: Declaring
const fetchUsers = async () => {...}avoids returning a Promise directly touseEffect, which is not allowed. - Dependency Array: Passing variables in the
dependency array (e.g., a dynamic
userId) re-triggers the Axios request whenever those values change.
Handling Cleanup and Request Cancellation
When a component unmounts before an Axios request completes,
attempting to update state can cause memory leaks or unexpected
behavior. Axios integrates with the native AbortController
API, which can be hooked into the cleanup function of
useEffect.
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await axios.get('/api/data', {
signal: controller.signal,
});
setData(response.data);
} catch (err) {
if (axios.isCancel(err)) {
// Request was canceled; do not update error state
return;
}
setError(err.message);
}
};
fetchData();
// Cleanup: Cancel request if the component unmounts
return () => {
controller.abort();
};
}, [dependency]);Abstracting with Custom Hooks
To eliminate boilerplate across multiple components, Axios logic can be encapsulated into a custom React hook.
import { useState, useEffect } from 'react';
import axios from 'axios';
function useAxios(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const execute = async () => {
try {
setLoading(true);
const res = await axios.get(url, { signal: controller.signal });
setData(res.data);
} catch (err) {
if (!axios.isCancel(err)) {
setError(err.message);
}
} finally {
setLoading(false);
}
};
execute();
return () => controller.abort();
}, [url]);
return { data, loading, error };
}Integrating with Global State Management
When fetched data needs to be shared globally:
- React Context API: Dispatch actions to update
Context state inside the
tryblock of your Axios call. - Redux Toolkit: Use
createAsyncThunkwith Axios to automatically generate actions for pending, fulfilled, and rejected request states. - Axios Interceptors: Configure global headers (such
as
Authorizationtokens) and centralize error handling outside component trees.