React useEffect Hook – Complete Notes
The useEffect Hook is a built-in React Hook that allows functional components to synchronize with external systems, such as APIs, timers, browser events, subscriptions, and third-party libraries. It runs an Effect after React has committed a render and can optionally return a cleanup function to remove subscriptions, cancel timers, or release resources.
React JS Course: JustAcademy React JS Training Course
Course Demo: Register for React JS Course Demo
1. What Is the useEffect Hook?
The useEffect Hook is used when a component needs to synchronize with something outside React's normal rendering process. For example, it can fetch data from an API, start a timer, listen for browser events, or connect to a chat service.
Syntax:
import { useEffect } from 'react';
useEffect(() => {
// Effect logic
return () => {
// Optional cleanup logic
};
}, [dependencies]);
Explanation:
useEffect is the React Hook used to declare an Effect.
- The setup function contains the logic that synchronizes with an external system.
- The optional cleanup function undoes or stops what the setup function started.
- The dependency array determines when React needs to run the Effect again.
2. Why Do We Need useEffect?
React components render UI based on their props and state. Sometimes the component must also interact with external systems or perform work after rendering. The useEffect Hook helps manage these operations.
Common use cases:
- Fetching information from an API.
- Setting up timers using
setInterval() or setTimeout().
- Adding and removing browser event listeners.
- Subscribing to external data sources.
- Connecting to a chat server or WebSocket.
- Synchronizing a third-party widget with React state.
- Managing document title changes or other browser-side synchronization.
Important: Do not use useEffect for every calculation or user interaction. If a value can be calculated directly during rendering, or an operation belongs in a button click handler, an Effect may not be necessary.
3. How Does useEffect Work?
- React renders the component.
- React commits the changes to the UI.
- The Effect setup function runs when required.
- If the dependencies change, React runs the previous cleanup function first, then runs the new setup function.
- When the component is removed, React runs the final cleanup function if one was provided.
Basic flow:
Component renders
↓
React commits the UI
↓
Effect setup function runs
↓
Dependencies change?
↓
Previous cleanup runs
↓
New Effect setup runs
↓
Component is removed
↓
Final cleanup runs
Effects run only on the client, not during server rendering. Their exact timing relative to browser painting can vary depending on the situation.
4. Example: Basic useEffect Hook
This example changes the browser tab title when the component mounts.
import { useEffect } from 'react';
function Welcome() {
useEffect(() => {
document.title = 'Welcome to React';
}, []);
return <h1>Welcome to React!</h1>;
}
export default Welcome;
Explanation:
useEffect declares an Effect.
document.title changes the browser tab title.
- The empty dependency array means the Effect does not re-run because of changes to component props or state.
- The Effect runs on the client after the initial commit, subject to React's development behavior.
5. useEffect Without a Dependency Array
When you omit the dependency array, the Effect runs after every commit of the component.
import { useEffect, useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
});
return (
<div>
<h2>Count: {count}</h2>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default Counter;
Explanation: Whenever the component commits a render, the Effect sets the document title to the current count.
Note: An Effect without dependencies can run frequently. Avoid updating state inside it without a clear reason, because repeated state updates can cause an infinite render cycle.
6. useEffect With an Empty Dependency Array
An empty dependency array, [], tells React that the Effect has no reactive dependencies. It normally runs after the component's initial commit and does not re-run due to ordinary state or prop changes.
import { useEffect } from 'react';
function App() {
useEffect(() => {
console.log('Component initialized');
}, []);
return <h1>React Application</h1>;
}
export default App;
Important: In development Strict Mode, React may run an extra setup-and-cleanup cycle to help detect bugs. Therefore, an Effect with [] should not be treated as a guarantee that its setup function executes exactly once in every environment.
7. useEffect With Dependencies
A dependency array tells React which reactive values the Effect uses. React compares each dependency with its previous value using Object.is. If a dependency changes, React runs the cleanup function from the previous Effect and then runs the new setup function.
useEffect(() => {
// Synchronize using the current userId
}, [userId]);
Example:
import { useEffect, useState } from 'react';
function UserProfile() {
const [userId, setUserId] = useState(1);
useEffect(() => {
console.log('Selected user:', userId);
}, [userId]);
return (
<div>
<p>User ID: {userId}</p>
<button onClick={() => setUserId(previous => previous + 1)}>Next User</button>
</div>
);
}
export default UserProfile;
Explanation: The Effect runs after the initial commit and again after commits where userId changes.
8. Difference Between Dependency Array Options
| Syntax |
When the Effect Runs |
Example Use |
useEffect(() => {}) |
After every commit |
Uncommon; use only when required |
useEffect(() => {}, []) |
After the initial client commit; development Strict Mode may repeat setup and cleanup |
Initialize a client-side integration with proper cleanup |
useEffect(() => {}, [value]) |
After the initial commit and commits where a dependency changes |
Synchronize an external system with a changing value |
useEffect(() => {}, [a, b]) |
When either dependency changes, after the initial commit |
Synchronize using multiple values |
9. Cleanup Function in useEffect
A cleanup function stops or reverses the work performed by the Effect setup. It is important when an Effect creates a timer, subscribes to events, opens a connection, or starts another operation that must be cleaned up.
Syntax:
useEffect(() => {
// Setup logic
return () => {
// Cleanup logic
};
}, [dependencies]);
Example: Browser event listener
import { useEffect, useState } from 'react';
function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function updateWidth() {
setWidth(window.innerWidth);
}
window.addEventListener('resize', updateWidth);
return () => {
window.removeEventListener('resize', updateWidth);
};
}, []);
return <h2>Window Width: {width}px</h2>;
}
export default WindowWidth;
Explanation:
- The component reads the browser's current window width.
- The Effect registers a resize event listener.
- When the browser window changes size, the listener updates state.
- The cleanup function removes the listener when the Effect is cleaned up.
Note: This example accesses window, so it is intended for client-side rendering. For server-rendered applications, initialize browser-dependent state safely on the client.
10. Example: Timer Using useEffect
The useEffect Hook can manage an interval that updates a counter every second.
import { useEffect, useState } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const timerId = setInterval(() => {
setSeconds(previousSeconds => previousSeconds + 1);
}, 1000);
return () => {
clearInterval(timerId);
};
}, []);
return <h2>Seconds: {seconds}</h2>;
}
export default Timer;
Explanation: The interval increments the state every second. The functional updater uses the previous state value. The cleanup function clears the interval so it does not continue running after the Effect is cleaned up.
11. Fetching API Data Using useEffect
One common use of useEffect is fetching information from an API. A component can display loading, success, and error states while retrieving data.
Example:
import { useEffect, useState } from 'react';
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
const controller = new AbortController();
async function fetchUsers() {
try {
setLoading(true);
setError('');
const response = await fetch(
'https://jsonplaceholder.typicode.com/users',
{ signal: controller.signal }
);
if (!response.ok) {
throw new Error('Failed to fetch users');
}
const data = await response.json();
setUsers(data);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message || 'Something went wrong');
}
} finally {
if (!controller.signal.aborted) {
setLoading(false);
}
}
}
fetchUsers();
return () => {
controller.abort();
};
}, []);
if (loading) return <p>Loading users...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div>
<h2>User List</h2>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
export default Users;
Explanation:
useState stores the users, loading status, and error message.
useEffect starts the API request after the initial commit.
fetch() requests the data from the API.
response.ok checks whether the HTTP response indicates success.
response.json() parses the response body.
AbortController cancels the request when the Effect is cleaned up.
- The UI displays loading, error, or user-list content as appropriate.
Best practice: For larger applications, consider framework-provided data loading or a data-fetching library that supports caching, deduplication, and server rendering. Manual fetching in Effects is useful for learning and certain client-side use cases, but it is not always the most efficient approach.
12. Updating the Effect When Props Change
When an Effect depends on a prop, include that prop in the dependency array. This allows the Effect to synchronize with the latest prop value.
import { useEffect } from 'react';
function Product({ productId }) {
useEffect(() => {
console.log('Selected product:', productId);
}, [productId]);
return <h2>Product ID: {productId}</h2>;
}
export default Product;
Explanation: The Effect runs after the initial commit and when the component receives a changed productId.
13. Rules of the useEffect Hook
- Call useEffect only at the top level of a function component or custom Hook.
- Do not call useEffect inside conditions, loops, or nested functions.
- Include all reactive values used by the Effect in its dependency array.
- Keep the dependency list inline and maintain a constant number of dependencies.
- Return a cleanup function when the Effect starts work that must be stopped or undone.
- Keep setup and cleanup logic symmetrical.
- Do not make the Effect callback itself
async, because an Effect callback should return either nothing or a cleanup function, not a Promise.
- Use event handlers for work that should happen directly because a user clicked or submitted something.
14. Common Mistakes in useEffect
| Mistake |
Problem |
Better Approach |
| Missing dependencies |
Effect may use outdated props or state |
Include all required reactive dependencies |
| Infinite render loop |
Effect updates state, which changes its dependencies and repeats the Effect |
Review whether the Effect is needed and why dependencies change |
| Forgetting cleanup |
Timers or listeners may remain active unnecessarily |
Return cleanup logic where required |
| Making the callback async |
An async callback returns a Promise instead of a cleanup function |
Define and call an async function inside the Effect |
| Using Effects for simple calculations |
Creates unnecessary renders and duplicated state |
Calculate derived values during rendering |
| Suppressing dependency warnings |
Can hide stale-value bugs |
Refactor the Effect and declare correct dependencies |
| Assuming setup runs exactly once |
Development Strict Mode may run an extra setup-cleanup cycle |
Write Effects that clean up correctly |
15. Incorrect vs. Correct useEffect Syntax
Incorrect: async Effect callback
useEffect(async () => {
const response = await fetch('/api/data');
}, []);
Correct: async function inside the Effect
useEffect(() => {
async function loadData() {
const response = await fetch('/api/data');
const data = await response.json();
console.log(data);
}
loadData();
}, []);
Note: A production-ready data-fetching Effect should also handle errors and clean up or ignore obsolete requests when necessary.
16. useEffect vs. useLayoutEffect
| Feature |
useEffect |
useLayoutEffect |
| Timing |
Generally runs after the browser can paint the updated screen |
Runs after DOM changes but before the browser repaints |
| Common purpose |
API requests, subscriptions, timers, external synchronization |
Measuring layout or positioning visual elements before paint |
| Performance |
Preferred for most Effects |
Can block painting, so use only when necessary |
17. useEffect vs. Event Handlers
| useEffect |
Event Handler |
| Synchronizes a component with an external system |
Responds directly to a specific user interaction |
| Runs according to the component's lifecycle and dependencies |
Runs when an event occurs |
| Example: subscribe to a browser event |
Example: submit a form after a button click |
18. How to Avoid Infinite Loops
An infinite loop can occur when an Effect updates state and that update changes one or more dependencies, causing the Effect to run again repeatedly.
Example of a problematic pattern:
useEffect(() => {
setCount(count + 1);
}, [count]);
This Effect increments the count every time the count changes, which causes another render and another increment.
Better approach: If the count should change only when the user clicks a button, update it in the event handler instead of using an Effect.
<button onClick={() => setCount(previous => previous + 1)}>
Increment
</button>
19. Practical Project: Live Clock
Project objective: Build a live clock that updates every second using useEffect and useState.
Code:
import { useEffect, useState } from 'react';
function LiveClock() {
const [time, setTime] = useState(() => new Date());
useEffect(() => {
const intervalId = setInterval(() => {
setTime(new Date());
}, 1000);
return () => {
clearInterval(intervalId);
};
}, []);
return (
<div>
<h1>Live Clock</h1>
<h2>{time.toLocaleTimeString()}</h2>
</div>
);
}
export default LiveClock;
Learning outcomes:
- Use useState to store the current time.
- Use useEffect to create a timer.
- Update state at regular intervals.
- Use a cleanup function to stop the interval.
20. Practice Exercises
- Create a component that updates the document title whenever a counter changes.
- Fetch and display a list of users from a public API.
- Create a timer that starts when a component appears and stops when it is removed.
- Add and remove a window resize event listener.
- Build a component that displays different information when a selected ID changes.
- Create a live clock that updates every second.
- Connect a component to an external service and clean up the connection correctly.
- Find and fix an Effect that creates an infinite render loop.
21. Interview Questions and Answers
Q1. What is the useEffect Hook?
Answer: useEffect is a React Hook that synchronizes a component with external systems, such as APIs, timers, event listeners, and subscriptions.
Q2. What is the syntax of useEffect?
Answer: useEffect(setup, dependencies), where the setup function contains the Effect logic and the optional dependency array controls when the Effect re-runs.
Q3. What happens if we omit the dependency array?
Answer: The Effect runs after every commit of the component.
Q4. What does an empty dependency array mean?
Answer: The Effect has no reactive dependencies and normally runs after the initial client commit. Development Strict Mode can run an extra setup-and-cleanup cycle.
Q5. What is a cleanup function?
Answer: A cleanup function undoes or stops work started by the Effect, such as clearing a timer or removing an event listener.
Q6. Why are dependencies important?
Answer: Dependencies tell React which reactive values the Effect uses. When a dependency changes, React can run cleanup for the previous Effect and then run the updated setup.
Q7. Can useEffect be declared async?
Answer: The Effect callback itself should not be async because it must return nothing or a cleanup function. Define an async function inside the Effect and call it instead.
Q8. Why does an infinite loop happen in useEffect?
Answer: It can happen when the Effect updates state and the update changes a dependency, causing the Effect to repeat continuously.
Q9. Does useEffect run during server rendering?
Answer: No. Effects run on the client after React commits the component.
Q10. When should we avoid useEffect?
Answer: Avoid it for values that can be calculated during rendering or for actions that belong directly in an event handler. Use Effects when synchronization with an external system is needed.
22. Quick Revision Checklist
- Understand the purpose and syntax of useEffect.
- Know the difference between no dependency array, an empty array, and a dependency array with values.
- Understand setup and cleanup functions.
- Practice timers and browser event listeners.
- Understand API fetching, loading states, and error handling.
- Include all required reactive dependencies.
- Avoid unnecessary Effects and infinite render loops.
- Understand development Strict Mode behavior.
- Know the difference between useEffect, useLayoutEffect, and event handlers.
- Build practical projects using useEffect and useState together.
23. Conclusion
The useEffect Hook is an essential React feature for synchronizing components with external systems. By learning dependency arrays, setup and cleanup functions, API requests, timers, and event listeners, developers can build interactive React applications while avoiding common issues such as memory leaks, stale values, and infinite render loops.
Learn more through the JustAcademy React JS Training Course or register for a React JS course demo.