Clean β’ Professional
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.
useEffect Hook?The useEffect hook in React is used to run code after a component has rendered.
Itβs ideal for performing tasks like:
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 when variable changesLetβ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;
useEffect hook triggers the data fetch.loading is true, so a loading message displays.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:
try...catchSometimes, 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
}, []);
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
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>;
}Β