You are viewing a free preview of this lesson.
Subscribe to unlock all 10 lessons in this course and every other course on LearningBro.
Not everything in a React app is pure rendering. You often need to fetch data from APIs, set up subscriptions, or interact with browser APIs. The useEffect hook lets you synchronise your component with these side effects. This lesson covers useEffect, fetching data, loading/error states, cleanup, custom hooks, and an overview of SWR and React Query.
A side effect is anything that reaches outside the component's rendering logic:
| Side Effect | Example |
|---|---|
| Fetching data | Calling an API on mount |
| Subscriptions | WebSocket connections, event listeners |
| Timers | setTimeout, setInterval |
| DOM manipulation | Focusing an input, measuring layout |
| Logging / analytics | Tracking page views |
import { useState, useEffect } from "react";
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
// Cleanup function — runs when the component unmounts
return () => clearInterval(interval);
}, []); // Empty dependency array = run once on mount
return <p>Elapsed: {seconds}s</p>;
}
| Dependency Array | When Effect Runs |
|---|---|
useEffect(fn) | After every render |
useEffect(fn, []) | Once — on mount only |
useEffect(fn, [a,b]) | When a or b changes |
Rule: Include every value from the component scope that the effect uses in the dependency array.
interface Post {
id: number;
title: string;
body: string;
}
function PostList() {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
async function fetchPosts() {
try {
const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
signal: controller.signal,
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data: Post[] = await res.json();
setPosts(data);
} catch (err) {
if (err instanceof Error && err.name !== "AbortError") {
setError(err.message);
}
} finally {
setLoading(false);
}
}
fetchPosts();
return () => controller.abort();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Component mounts
│
▼
useEffect runs
│
▼
fetch() request ──▶ API
│
▼
Response arrives
│
├── Success ──▶ setPosts(data), setLoading(false)
│
└── Error ──▶ setError(message), setLoading(false)
│
▼
Component re-renders with new state
The function you return from useEffect is the cleanup function. React calls it:
useEffect(() => {
const handleResize = () => console.log(window.innerWidth);
window.addEventListener("resize", handleResize);
Subscribe to continue reading
Get full access to this lesson and all 10 lessons in this course.