useEffect Execution in React JS
useEffect Execution is an important concept in React JS that explains when and how the useEffect() Hook runs during a component's lifecycle. It helps developers perform side effects such as fetching API data, setting up timers, adding event listeners, interacting with browser APIs, and synchronizing React components with external systems.
These notes cover the execution flow of useEffect(), dependency arrays, cleanup functions, execution order, practical examples, common mistakes, interview questions, and best practices.
Course Link: JustAcademy React JS Training Course
Demo Link: Register for React JS Course Demo
1. What Is useEffect Execution?
useEffect() is a React Hook that allows a functional component to synchronize with external systems. Execution refers to when React runs the Effect's setup function and, when necessary, its cleanup function.
React generally runs an Effect after the component's render has been committed to the screen. The Effect may execute after the initial mount and again when its specified dependencies change.
Example
import { useEffect } from 'react';
function App() {
useEffect(() => {
console.log("Effect executed");
});
return <h1>Hello React</h1>;
}
export default App;
Explanation: Because no dependency array is provided, the Effect runs after every committed render. If the component re-renders, the Effect runs again.
2. Syntax of useEffect()
useEffect(() => {
// Effect setup code
return () => {
// Optional cleanup code
};
}, [dependencies]);
- Setup function: Contains the side-effect logic that React should execute.
- Cleanup function: Optionally stops or reverses the work performed by the setup function.
- Dependency array: Specifies the reactive values that determine when the Effect needs to run again.
The cleanup function is optional. If the Effect returns no cleanup function, React has nothing from that Effect to clean up.
3. How Does useEffect Execute?
The execution of an Effect can be understood through the following steps:
- Initial render: React calls the component function to calculate the UI.
- Commit: React applies the required changes to the DOM.
- Effect setup: React runs the Effect's setup function at the appropriate time after the commit.
- Dependency comparison: On later commits, React compares the current dependency values with the previous values using
Object.is.
- Cleanup and re-execution: If dependencies have changed, React runs the previous cleanup function first, then runs the new setup function.
- Unmount: React runs the final cleanup function when the component is removed.
Important: Effects run only on the client, not during server rendering. Their exact timing relative to browser painting can vary depending on how the update was triggered.
4. useEffect Execution with Different Dependency Arrays
4.1 Without a Dependency Array
When the dependency array is omitted, the Effect runs after every committed render.
import { useEffect, useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("Effect executed");
});
return (
<div>
<h2>Count: {count}</h2>
<button onClick={() => setCount(count + 1)}>
Increase
</button>
</div>
);
}
export default Counter;
Execution: The Effect runs after the initial commit and again after each committed update caused by clicking the button.
4.2 With an Empty Dependency Array
An empty dependency array tells React that the Effect has no reactive dependencies. It runs after the initial commit and does not re-run merely because the component's state or props change.
import { useEffect, useState } from 'react';
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("Initial Effect setup");
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increase
</button>
</div>
);
}
export default App;
Execution: The Effect runs after the initial commit. Clicking the button re-renders the component, but the Effect does not run again because its dependency list is empty.
Note: In development, React Strict Mode can run an additional setup-and-cleanup cycle. Therefore, an empty dependency array does not guarantee that you will see exactly one setup execution in development.
4.3 With a Dependency Array
When dependencies are provided, the Effect runs after the initial commit and runs again after a commit when at least one dependency changes.
import { useEffect, useState } from 'react';
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("Count changed:", count);
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increase
</button>
</div>
);
}
export default App;
Execution: The Effect runs after the initial commit and again when count changes. A re-render that leaves count unchanged does not, by itself, cause this Effect to re-run.
Dependency Comparison Table
| Effect syntax |
When it executes |
Typical use |
useEffect(() => {}) |
After every committed render |
Work that must synchronize after every commit, when genuinely needed |
useEffect(() => {}, []) |
After initial commit; development Strict Mode may repeat setup for testing |
Set up a client-side subscription or connection with cleanup |
useEffect(() => {}, [count]) |
After initial commit and when count changes |
Synchronize with a system that depends on the count |
useEffect(() => {}, [a, b]) |
After initial commit and when either dependency changes |
Synchronize using multiple reactive values |
5. Understanding Effect Execution and Component Rendering
Rendering and Effect execution are two different processes. A component can render without its Effect re-running when the dependency values have not changed.
import { useEffect, useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
const [name, setName] = useState("React");
useEffect(() => {
console.log("Count Effect:", count);
}, [count]);
return (
<div>
<p>Count: {count}</p>
<p>Name: {name}</p>
<button onClick={() => setCount(count + 1)}>
Increase Count
</button>
<button onClick={() => setName("JavaScript")}>
Change Name
</button>
</div>
);
}
export default Example;
Explanation: Changing count causes the Effect to run again after the update. Changing only name re-renders the component, but does not re-run this Effect because count has not changed.
6. Cleanup Function Execution
A cleanup function is used to stop or undo work started by an Effect. It is especially important for timers, subscriptions, browser event listeners, and network requests.
Example: Timer Cleanup
import { useEffect, useState } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const timerId = setInterval(() => {
setSeconds(previous => previous + 1);
}, 1000);
return () => {
clearInterval(timerId);
};
}, []);
return <h2>Seconds: {seconds}</h2>;
}
export default Timer;
Explanation: React runs the Effect setup after the initial commit and starts the timer. When the component unmounts, the cleanup function clears the interval. In development Strict Mode, cleanup may also run during the extra setup-and-cleanup test.
When Does Cleanup Execute?
- Before the Effect re-runs because one or more dependencies changed.
- When the component is removed from the page.
- During the additional development-only Strict Mode cycle, when enabled.
Cleanup is not limited to unmounting. When dependencies change, cleanup uses the values from the previous Effect execution, and the next setup uses the new values.
7. Execution Order with Dependencies
Consider an Effect that depends on a selected product ID:
useEffect(() => {
console.log("Loading product:", productId);
return () => {
console.log("Cleaning up product:", productId);
};
}, [productId]);
If productId changes from 1 to 2, the normal sequence is:
- React commits the UI update.
- The previous Effect cleanup runs with the old
productId, which is 1.
- The new Effect setup runs with
productId equal to 2.
React may schedule passive Effects relative to browser painting depending on the update. Do not rely on useEffect for precise visual timing.
8. Practical Example: Fetching API Data with useEffect
The following example demonstrates how an Effect can fetch data when a component loads and how cleanup can prevent an outdated response from updating the UI.
import { useEffect, useState } from 'react';
function Users() {
const [users, setUsers] = useState([]);
const [error, setError] = useState("");
useEffect(() => {
let ignore = false;
async function loadUsers() {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users"
);
if (!response.ok) {
throw new Error("Failed to fetch users");
}
const data = await response.json();
if (!ignore) {
setUsers(data);
}
} catch (err) {
if (!ignore) {
setError(err.message);
}
}
}
loadUsers();
return () => {
ignore = true;
};
}, []);
if (error) return <p>Error: {error}</p>;
return (
<div>
<h2>User List</h2>
{users.map(user => (
<p key={user.id}>{user.name}</p>
))}
</div>
);
}
export default Users;
Explanation: The Effect starts the request after the initial commit. The cleanup sets ignore to true, preventing a late response from updating state after cleanup. This pattern ignores stale results; it does not cancel the underlying request. An AbortController can be used when you also want to abort a supported fetch request.
9. Execution in React Strict Mode
React Strict Mode helps detect problems in development. When Strict Mode is enabled, React performs an extra setup-and-cleanup cycle for Effects during the initial mount to check whether cleanup correctly reverses setup.
import { useEffect } from 'react';
function App() {
useEffect(() => {
console.log("Setup executed");
return () => {
console.log("Cleanup executed");
};
}, []);
return <h1>Strict Mode Example</h1>;
}
export default App;
With Strict Mode enabled in development, you may see the following sequence:
Setup executed
Cleanup executed
Setup executed
This is a development check, not the normal production sequence. Avoid trying to hide the extra execution with a ref. Instead, ensure the cleanup correctly undoes the setup. For example, unsubscribe when subscribing and clear a timer when starting it.
10. Common Problems in useEffect Execution
10.1 Infinite Re-Rendering
useEffect(() => {
setCount(count + 1);
});
Problem: The Effect runs after every commit and updates state. That state update triggers another render, which can cause the Effect to run again indefinitely.
Solution: Decide whether the Effect is necessary. If the state can be calculated directly during rendering, remove the Effect. Otherwise, synchronize state with an external system and update state only when needed.
10.2 Missing Dependencies
useEffect(() => {
console.log(userId);
}, []);
Problem: If userId is a reactive value used by the Effect, omitting it can leave the Effect using an outdated value.
Solution: Include userId in the dependency array, unless you restructure the code so it is no longer a reactive dependency.
useEffect(() => {
console.log(userId);
}, [userId]);
10.3 Unnecessary Re-Execution
Objects and functions created during rendering can have a new identity on every render. If used as dependencies, they can cause an Effect to run more often than intended.
Solution: Prefer creating Effect-specific objects and helper functions inside the Effect when appropriate. Use useMemo or useCallback only when needed, and follow the dependency linter's guidance.
10.4 Forgetting Cleanup
Failing to remove event listeners or clear timers can cause duplicated work, memory leaks, or unexpected behavior.
Solution: Return a cleanup function that removes listeners, clears timers, disconnects external connections, or cancels supported asynchronous work.
11. Best Practices for useEffect Execution
- Call
useEffect() at the top level of a React component or custom Hook.
- Include all reactive values used by the Effect in its dependency array.
- Use cleanup for subscriptions, timers, connections, and other work that needs to be stopped.
- Do not use Effects unnecessarily to calculate values that can be derived during rendering.
- Do not suppress dependency warnings just to prevent an Effect from re-running.
- Design setup and cleanup so that repeating the cycle does not cause incorrect behavior.
- Use
useLayoutEffect() instead of useEffect() only when work must happen before the browser repaints, such as certain layout measurements.
- Consider framework data-fetching features or a dedicated data-fetching library for more complex API workflows.
12. Difference Between useEffect and useLayoutEffect
| Feature |
useEffect |
useLayoutEffect |
| Timing |
Runs as a passive Effect after commit; often after browser paint |
Runs after DOM mutations but before browser repaint |
| Typical use |
API synchronization, subscriptions, timers, browser events |
Layout measurement and visual adjustments that must avoid flicker |
| Painting |
Generally does not block painting |
Blocks repaint while its code and synchronous updates run |
| Performance |
Usually preferred for ordinary Effects |
Use sparingly because it can affect responsiveness |
13. Real-World Uses of useEffect Execution
- API integration: Fetch data when a component loads or a query changes.
- Timers: Start and clear countdowns or intervals.
- Event listeners: Listen for window resize, keyboard, or scroll events and remove listeners during cleanup.
- Subscriptions: Connect and disconnect from a chat server or external data source.
- Third-party libraries: Synchronize a React component with a map, chart, animation, or media player.
- Browser APIs: Synchronize with browser features that React does not manage directly.
14. Interview Questions and Answers
Q1. What is useEffect execution in React?
Answer: It is the process through which React runs an Effect's setup function after a component commits and re-runs it when required by dependency changes.
Q2. When does useEffect execute?
Answer: It runs after the initial commit and, depending on its dependency array, after later commits. Without a dependency array, it runs after every committed render.
Q3. What happens when the dependency array is empty?
Answer: The Effect does not re-run merely because props or state change. It runs after the initial commit, although development Strict Mode may perform an extra setup-and-cleanup cycle.
Q4. Why is a cleanup function needed?
Answer: Cleanup reverses or stops work started by the Effect, such as clearing timers, removing event listeners, and disconnecting subscriptions.
Q5. When does cleanup execute?
Answer: Before the Effect re-runs with changed dependencies and when the component unmounts. Development Strict Mode can also run cleanup during its extra test cycle.
Q6. Why can useEffect cause an infinite loop?
Answer: An Effect may update state, causing a re-render that makes the Effect run again. If this repeats without a stopping condition, it creates an infinite loop.
Q7. Does useEffect run during server-side rendering?
Answer: No. Effects run on the client, not during server rendering.
Q8. What is the difference between useEffect and useLayoutEffect?
Answer: useEffect is intended for passive synchronization work, while useLayoutEffect runs before the browser repaints and is useful for certain layout measurements and visual updates.
Q9. What does React use to compare dependencies?
Answer: React compares each dependency with its previous value using Object.is.
Q10. Should every component use useEffect?
Answer: No. Use it when synchronization with an external system is required. Derived values and ordinary event-handling logic often do not need an Effect.
15. Practice Exercises
- Create a component that logs a message after its initial commit using an empty dependency array.
- Create a counter whose Effect logs the latest count whenever it changes.
- Create a timer that increments every second and clears the interval during cleanup.
- Add a window resize listener and remove it when the component unmounts.
- Fetch a list of products and handle loading, success, and error states.
- Use React Strict Mode and verify that your Effect cleanup correctly mirrors its setup.
16. Quick Revision Checklist
- Understand the purpose of
useEffect().
- Know the difference between no dependency array, an empty array, and a populated dependency array.
- Understand initial setup, dependency changes, cleanup, and unmounting.
- Know why Strict Mode can run an extra setup-and-cleanup cycle in development.
- Recognize infinite loops and missing dependency problems.
- Practice API fetching, timers, event listeners, and subscriptions.
17. Conclusion
useEffect() helps React components synchronize with external systems. Correct execution depends on understanding commits, dependency comparisons, setup functions, and cleanup functions. Using dependencies accurately and cleaning up external work makes React applications more predictable, maintainable, and reliable.
For further learning, read the official React useEffect Documentation and Synchronizing with Effects.
Learn React JS with JustAcademy: Explore React JS Training | Register for a Course Demo