useEffect Hook
useEffect Hook — Handling side effects in React
In React, all data flow occurs during the render phase. However, real applications often have operations that are connected to the external world — for example: loading data from an API, adding event listeners, working with localStorage, or changing the document title. All these operations are called side effects.
useEffect is a hook that allows you to write code that will run after rendering.
It tells React: "when this component appears or updates, perform this action".
useEffect(() => {
// write your side effect here
return () => {
// cleanup (optional)
};
}, [dependencies]);
If you don't provide the second argument (dependency array), the effect will run after every render.
import React, { useState, useEffect } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Component rendered, count =', count);
});
return (
Count: {count}
);
}
Every time you click the button, the effect runs again. This is usually inefficient unless you specifically want to track all state changes.
If you pass an empty array [], the effect will run only once — during the component's initial mount.
This is very common for loading data from an API.
import React, { useState, useEffect } from 'react';
export default function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
console.log('Fetching data...');
fetch('https://jsonplaceholder.typicode.com/users')
.then(res => res.json())
.then(data => setUsers(data));
}, []); // runs once
return (
Users
{users.map(u => - {u.name}
)}
);
}
Important: The dependency array is empty, meaning the effect won't run when state changes.
If the effect should run when certain state changes, specify it in the dependency array.
import React, { useState, useEffect } from 'react';
export default function Timer() {
const [seconds, setSeconds] = useState(0);
const [running, setRunning] = useState(false);
useEffect(() => {
if (!running) return;
const id = setInterval(() => setSeconds(s => s + 1), 1000);
console.log('Timer started...');
return () => clearInterval(id); // cleanup
}, [running]);
return (
Time: {seconds} seconds
);
}
When running becomes true — setInterval starts working.
When it becomes false — cleanup (clearInterval) stops the timer.
Cleanup helps React clean up the consequences left by the effect when the component is removed from the DOM or the dependency changes. For example: removing event listeners or canceling timers.
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
Wrong approach: adding event listeners without cleanup — this can cause memory leaks.
If you forget to include certain state in the dependency array, the effect might use stale values.
useEffect(() => {
console.log('Current count:', count);
}, []); // ❌ count will never update here
The correct version is:
useEffect(() => {
console.log('Current count:', count);
}, [count]); // ✅ effect will run every time count changes
Here's a professional-level example: data loading, state management, and error handling.
import React, { useState, useEffect } from 'react';
export default function Posts() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchPosts() {
try {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!res.ok) throw new Error('Network error');
const data = await res.json();
setPosts(data.slice(0, 5));
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchPosts();
}, []); // only on mount
if (loading) return Loading...
;
if (error) return Error: {error}
;
return (
Latest Posts
{posts.map(p => - {p.title}
)}
);
}
This demonstrates best practices: async operations inside effects and managing loader/error states.
Write a component that: