React useEffect for Data Fetching
When building dynamic React apps, you’ll often need to fetch data from APIs — for example, loading users, posts, or products. However, fetching data is considered a side effect because it happens outside the normal rendering process.
That’s where the useEffect hook comes in — it allows React components to perform side effects such as fetching data, setting up subscriptions, or updating the DOM safely and efficiently.
What is the useEffect Hook?
The useEffect hook in React is used to run code after a component has rendered.
It’s ideal for performing tasks like:
- Fetching data from an API
- Updating the document title
- Setting up event listeners or timers
- Cleaning up side effects (e.g., aborting a fetch or removing a listener)
Syntax:
useEffect(() => {
// side effect logic here
return () => {
// optional cleanup code
};
}, [dependencies]);
The dependency array determines when the effect runs:
[]→ Runs once on mount[variable]→ Runs whenvariablechanges- No array → Runs on every render (usually not recommended for data fetching)
Fetching Data with useEffect
Let’s see how to fetch data from an API using the fetch() method inside useEffect.
import React, { useEffect, useState } from "react";
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch("<https://jsonplaceholder.typicode.com/users>")
.then(response => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.json();
})
.then(data => setUsers(data))
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, []); // runs only once when component mounts
if (loading) return <p>Loading users...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
export default Users;
How it works:
- When the component mounts, the
useEffecthook triggers the data fetch. - While fetching,
loadingis true, so a loading message displays. - Once data is fetched, the component updates and displays the list of users.
- If there’s an error, the error message is shown instead.
Using Async/Await with useEffect
Since useEffect cannot directly use an async function as its callback (it expects a cleanup function or nothing), you can define an async function inside it and call it:
import React, { useEffect, useState } from "react";
function Posts() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchPosts = async () => {
try {
const res = await fetch("<https://jsonplaceholder.typicode.com/posts>");
if (!res.ok) throw new Error("Failed to fetch posts");
const data = await res.json();
setPosts(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchPosts();
}, []); // runs once on mount
if (loading) return <p>Loading posts...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
);
}
Benefits of Async/Await:
- Cleaner and more readable code
- Easier to handle multiple API calls
- Natural error handling with
try...catch
Cleaning Up Side Effects (Abort Fetch Requests)
Sometimes, a component unmounts before an API call finishes — this can cause warnings like
“Can’t perform a React state update on an unmounted component.”
You can prevent this by aborting the fetch when the component unmounts:
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const res = await fetch("<https://jsonplaceholder.typicode.com/todos>", {
signal: controller.signal,
});
const data = await res.json();
setTodos(data);
} catch (err) {
if (err.name !== "AbortError") {
console.error("Fetch failed:", err);
}
}
};
fetchData();
return () => controller.abort(); // cleanup on unmount
}, []);
Refetching Data When Dependencies Change
You can refetch data whenever certain values change by adding dependencies to the array:
useEffect(() => {
fetch(`https://api.example.com/users?page=${page}`)
.then(res => res.json())
.then(data => setUsers(data));
}, [page]); // refetches whenever 'page' changes
Example – Reusable Data Fetching Hook with useEffect
import { useState, useEffect } from "react";
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const getData = async () => {
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error("Network error");
const json = await response.json();
setData(json);
} catch (err) {
if (err.name !== "AbortError") setError(err.message);
} finally {
setLoading(false);
}
};
getData();
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
export default useFetch;
Usage:
function Products() {
const { data, loading, error } = useFetch("https://fakestoreapi.com/products");
if (loading) return <p>Loading products...</p>;
if (error) return <p>Error: {error}</p>;
return <ul>{data.map(item => <li key={item.id}>{item.title}</li>)}</ul>;
}
