By WebNest Studio Engineering Team
React Tutorial
The useEffect Hook
useEffect lets a function component run side effects — code that reaches outside of pure rendering, such as fetching data, subscribing to an external event source, manually interacting with the DOM, or setting up a timer. Rendering itself should stay pure (no side effects), so useEffect gives React a designated place to run that extra code after the component has rendered.
A useEffect call takes a function to run and an optional dependency array that controls when the effect re-runs. Getting the dependency array right is one of the most important — and most commonly misunderstood — parts of working with React.
Why it matters
Rendering should be pure; fetching data, starting timers and subscribing to events are side effects, and useEffect is where they go. It is also the hook most often misused. A missing dependency gives stale data, a missing clean-up gives memory leaks, and an effect that updates its own dependency gives an endless loop.
The Dependency Array
useEffect(() => { ... }, [dep1, dep2]) runs the effect after the initial render and again any time one of the listed dependencies changes between renders. An empty array [] means the effect runs only once, right after the first render, and never again. Omitting the array entirely makes the effect run after every single render, which is rarely what you want.
Cleanup Functions
If the function passed to useEffect returns another function, React treats that returned function as cleanup logic. React calls the cleanup function right before the effect runs again (when dependencies change) and again when the component unmounts. This is essential for anything that needs to be "undone" — clearing a timer, unsubscribing from a socket, or removing an event listener — to avoid memory leaks and duplicate subscriptions.
Common useEffect Use Cases
useEffect is most often used to synchronize a component with something outside of React's rendering model.
- Data fetching — calling an API when a component mounts or when an ID prop changes, then storing the result in state.
- Subscriptions — attaching a listener (e.g. to a WebSocket, window resize event, or third-party library) and cleaning it up on unmount.
- Manual DOM interaction — focusing an input, measuring an element's size, or integrating with non-React DOM libraries.
- Timers — starting an interval or timeout, and clearing it in the cleanup function so it does not keep running after the component is gone.
Example
Fetching data on mount and cleaning up a subscription on unmount
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let isCancelled = false;
async function fetchUser() {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
if (!isCancelled) setUser(data);
}
fetchUser();
return () => {
isCancelled = true; // cleanup avoids setting state after unmount or on a stale request
};
}, [userId]);
if (!user) return <p>Loading user...</p>;
return <p>{user.name}</p>;
}
export default UserProfile;
Initially renders "Loading user..."; once the fetch resolves, re-renders to show the fetched user's name. If userId changes before the previous fetch finishes, or the component unmounts, the cleanup function prevents the stale response from incorrectly updating state.
Common Mistakes
- Omitting the dependency array entirely, causing the effect (and often a data fetch) to re-run after every single render, sometimes creating an infinite loop.
- Listing an incomplete dependency array — leaving out a variable the effect actually uses — which causes the effect to use stale values from an earlier render.
- Forgetting to return a cleanup function for subscriptions or timers, leading to memory leaks or duplicate handlers stacking up across renders.
- Using useEffect for calculations that could be done directly during rendering, adding an unnecessary extra render cycle.
Exercise
Do this in src/App.jsx of a Vite + React project. Build a stopwatch: the number of seconds increases by one every second from the moment the component appears. Start the interval in an effect that runs once, and stop it when the component is removed.
Expected result: the page shows "Seconds: 0" and counts up by one each second. In development with Strict Mode, React runs the effect twice on mount, and the clean-up keeps only one interval alive.
import { useEffect, useState } from "react";
export default function App() {
const [seconds, setSeconds] = useState(0);
// TODO: an effect that starts an interval adding 1 every second,
// runs only on mount, and clears the interval on clean-up
return <p>Seconds: {seconds}</p>;
}
Show hints
- An empty dependency array,
[], makes the effect run once after the first render. - Return a function from the effect that calls
clearInterval(id), and update withsetSeconds((s) => s + 1).
Show solution
import { useEffect, useState } from "react";
export default function App() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <p>Seconds: {seconds}</p>;
}
Quick quiz
1. When does an effect with an empty dependency array run?
- A. After every render
- B. Never
- C. Once, after the component first mounts
- D. Only when the component unmounts
Show answer
C. Its clean-up function runs when the component unmounts.
2. What happens when the dependency array is left out entirely?
- A. The effect runs after every render
- B. The effect runs once
- C. The effect never runs
- D. React throws an error
Show answer
A. An effect that sets state without a dependency array can therefore loop for ever.
3. What is the function returned from an effect used for?
- A. To return data to the component
- B. To clean up: clear timers, remove listeners, cancel requests
- C. To re-run the effect
- D. To set state
Show answer
B. It runs before the effect runs again and when the component unmounts.
Interview questions
How does the dependency array of useEffect work?
Show answer
After each render, React compares every value in the array with its value from the previous render. If any has changed, the previous clean-up runs and then the effect runs again. An empty array means the effect runs once after mounting. No array means it runs after every render. Every value from the component that the effect reads should be listed; leaving one out makes the effect use a stale value.
Why does an effect run twice in development?
Show answer
In Strict Mode, React deliberately mounts each component, unmounts it and mounts it again in development, running the effect, its clean-up and the effect once more. This exposes effects that do not clean up properly, such as a subscription that is never removed. It does not happen in production. The correct response is to write the clean-up, not to remove Strict Mode.
Key Points to Remember
- useEffect runs side effects after rendering; its dependency array controls when it re-runs.
- An empty dependency array [] means "run once, after the first render only."
- A function returned from the effect is treated as cleanup, run before the next effect and on unmount.
- Typical use cases include data fetching, subscriptions, manual DOM work, and timers.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Shows a live preview of plain HTML, CSS and JavaScript. JSX needs a React project on your computer. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering