useEffect is a React hook that lets you perform side effects in functional components, replacing the lifecycle methods of class components: componentDidMount, componentDidUpdate, and componentWillUnmount. According to React Documentation (2025), useEffect runs after React has committed changes to the DOM, ensuring access to the actual DOM tree. The hook accepts an effect function and an optional dependency array that controls execution frequency.
Key Takeaways
useEffect is a hook added in React 16.8 for performing side effects in functional components. Side effects are operations not directly related to UI rendering: HTTP requests to APIs, event subscriptions, working with timers, DOM manipulations, logging, and integration with third-party libraries.
Before hooks, all these operations had to be placed in class component lifecycle methods: componentDidMount for initialization, componentDidUpdate for reacting to prop changes, componentWillUnmount for cleanup. useEffect unified all three scenarios in a single API, where the dependency array determines when the effect should run. This simplified logic and reduced code duplication, especially in subscription scenarios.
According to React DevTools Usage Survey (2024), useEffect is the second most popular hook after useState, used in 89% of React applications. Most developers use it for data fetching, synchronizing with external systems, and managing DOM event subscriptions.
import { useEffect } from 'react';
function UserProfile({ userId }) {
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
}
useEffect runs the passed effect function after React has finished rendering and updated the DOM. This is a key difference from render-time computations: the effect does not block rendering, which is critical for UX performance. If effects ran synchronously, users would see a frozen interface during data loading.
The lifecycle of a typical effect consists of three phases. On component mount, React executes the effect. On each update, if at least one dependency from the array has changed, React first runs the cleanup function of the previous effect, then the new effect. On component unmount, only the cleanup function runs.
According to React Team — useEffect RFC (2024), the internal implementation of useEffect uses a side effect queue in the fiber tree. After committing changes (commit phase), React walks through this queue and calls effect functions in the order they were declared in the component. Each fiber node stores a reference to the previous effect for correct cleanup and re-run.
| Stage | React Action | When It Runs |
|---|---|---|
| Mounting | Call effect function | After first render |
| Update | cleanup → effect | When dependencies change |
| Unmounting | Only cleanup | When component is removed |
The dependency array — the second argument to useEffect — determines when the effect should re-run. React compares each value in the array with the previous render using Object.is. If at least one value has changed, the effect runs again. If the array is empty ([]), the effect runs only once after mounting.
Choosing the right dependencies is the hardest part of working with useEffect. The array must include all variables and functions used inside the effect that can change between renders. Missing a dependency leads to stale closures — the effect sees an outdated value from the previous render. Including unnecessary dependencies causes excessive re-runs and potential bugs.
// Dependencies control when effect re-runs
useEffect(() => {
document.title = `User: ${user.name}`;
}, [user.name]); // re-run only when user.name changes
// eslint-disable-next-line react-hooks/exhaustive-deps
// If you omit a dependency, you get stale data
React provides the eslint-plugin-react-hooks with the exhaustive-deps rule, which automatically checks the completeness of the dependency array. According to Meta Engineering Blog (2024), enabling this plugin reduces hook-related bugs by 72%. It is recommended to fix all exhaustive-deps warnings rather than suppressing them with a comment, except in rare cases with custom logic.
If you omit the dependency array entirely, useEffect will run after every render. This can be useful for DOM synchronization or logging, but more often it is a mistake: the effect runs too frequently, leading to performance loss. In most cases, you should pass an empty array (run once on mount) or an array with specific props/state.
An empty array ([]) means the effect does not depend on any values and runs strictly once. This is the equivalent of componentDidMount in class components. However, keep in mind: if the effect uses props or state that are not listed in the dependency array, the effect will use their initial values and never see updates. This is called stale capture and is often a source of hard-to-find bugs.
| Dependency Array | Behavior | Class Equivalent |
|---|---|---|
| No argument | After every render | componentDidUpdate |
| [] | Once on mount | componentDidMount |
| [a, b] | When a or b change | ComponentWillReceiveProps analog |
| cleanup return | Manage unmounting | componentWillUnmount |
The cleanup function is a function that useEffect can return from its callback. React calls it on component unmount and before re-running the effect when dependencies change. Cleanup is necessary for canceling subscriptions, timers, requests, and any resources that must be released.
A typical example is a WebSocket subscription. On mount, a connection is created; on dependency update, it is recreated (cleanup closes the old one, the effect opens a new one); on unmount, it is closed. Without cleanup, each re-mount of the component would create a new WebSocket connection, leading to memory leaks and multiple connections.
useEffect(() => {
const socket = new WebSocket('wss://api.example.com');
socket.onmessage = event => setData(event.data);
// Cleanup function — runs on unmount and before re-run
return () => {
socket.close();
};
}, []);
According to React Documentation (2025), AbortController is the modern approach for canceling fetch requests in cleanup. If the effect makes an HTTP request and the component unmounts before it completes, the request continues running, and setState after unmount causes an error. Create an AbortController inside the effect and call controller.abort() in cleanup to cancel the request.
The most common mistake is missing dependencies. For example, the effect uses the userId prop, but the dependency array is empty. As a result, the effect runs once with the initial userId value and never reacts to its changes. The developer sees the component receiving a new userId, but the data does not update. eslint-plugin-react-hooks with the exhaustive-deps rule detects such bugs automatically.
// ❌ Race condition — no cancellation
useEffect(() => {
fetch(`/api/user/${userId}`).then(res => setUser(res));
}, [userId]);
// ✅ Fixed with AbortController
useEffect(() => {
const controller = new AbortController();
fetch(`/api/user/${userId}`, { signal: controller.signal })
.then(res => setUser(res));
return () => controller.abort();
}, [userId]);
To solve the infinite loop problem, avoid putting logic in useEffect that updates state based on previous state. Use the functional form of setState or move computations outside the effect. If the effect subscribes to storage or browser events, make sure the listener instance is created once, not on every render.
Frequently Asked Questions
Directly — no, because useEffect expects a synchronous function or undefined to be returned. If the callback is declared as async, it returns a Promise that React ignores, and the cleanup mechanism stops working. Solution: call an async function inside the effect: useEffect(() => { async function load() { ... }; load(); }, []).
There is no limit. React recommends separating unrelated logic into individual useEffect hooks, even if they have the same dependency array. Each effect should be responsible for one clearly defined side task: one for subscriptions, another for data loading, a third for synchronizing the tab title. This simplifies understanding and debugging.
In React Strict Mode (development mode only), all effects are mounted, unmounted, and mounted again. This is a feature, not a bug — React checks whether cleanup works correctly. If after unmounting and re-mounting the effect behaves incorrectly (e.g., duplicate subscriptions), your cleanup is incomplete. In production, the effect runs once.
Use AbortController. Create a controller inside the effect, pass controller.signal to fetch, and call controller.abort() in cleanup. If the component unmounts before the request completes, the fetch is canceled and setState will not be called. This prevents race conditions and the “Can't perform a React state update on an unmounted component” error.
useEffect will run after every render without exception. This means any setState inside the effect will cause a new render → new effect → infinite loop. In practice, an effect without a dependency array is almost always a mistake. Exceptions are logging or synchronizing with an external system where every render requires synchronization.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also