useEffect Hook - React Fundamentals
The useEffect hook is one of the most frequently tested topics in interviews. It serves as the primary tool for managing actions/tasks after a component renders (as shown on the screen). So, if you want to fetch data at the start, set a timer, or add an event listener - useEffect is the way to do it.
Here is a video with a brief introduction to useEffect, its definition, and an explanation of its dependency array along with an example:
The Anatomy of useEffect
A useEffect hook consists of three primary building blocks:
import { useEffect } from "react"
function MyComponent() {
useEffect(() => {
// side effect
return () => {
// clean-up function
}
}, [] /* Dependency array */)
// other component code
}
- Side effect function: The actual code that runs after the render cycle. This is the only required part of the hook.
- Dependency array: The optional parameter that dictates exactly when the effect function should execute.
- Cleanup function: The return value of the side effect, used to clear previous operations.
useEffect is called at the top level of a component to declare the effect. Calling useEffect itself has no return value, but a component can have more than one useEffect hook declared. This hook is used to synchronize with external systems outside of React, e.g., timers, API calls, event listeners, websockets, etc.
Side Effect: What is actually run
This is the main body of the useEffect hook - the first parameter passed to useEffect. It contains the tasks/statements executed after rendering (or on subsequent re-renders). The side effect callback is required and must be a function.
The diagram below visualizes the execution of useEffect when a component renders:

When
Strict ModeisONduring development, React will run an extra side effect cycle (effect + cleanup). This verifies that the cleanup logic actually cleans up (or resets) whatever was initialized in the side effect.
Dependency Array: Control the execution
As shown in the previous diagram, the dependency array decides when useEffect's side effect should execute. During a re-render, React checks whether the values given in the array have changed. Only then is the side effect executed.
How you define the dependency array changes how React handles your side effect:
- Omitted array: The side effect will execute on every single render of the component.
- Empty array (
[]): The effect runs only once when the component loads for the first time. - Array with values: The side effect runs only when those specific values change.
Now let's take an example: consider a component with two state variables, numA and numB, where incrementing either triggers a notification displaying their sum. When variables are placed in the dependency array, changing either value runs the effect.
Let us see the code and a running example below:
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { useState, useEffect } from "react";
export default function DependencyUseEffect() {
const [numA, setNumA] = useState(1);
const [numB, setNumB] = useState(1);
useEffect(() => {
toast.dismiss();
toast.info(`Sum ${numA} + ${numB} = ${numA + numB}`);
}, [numA, numB]);
return (
<div className="two-container">
<div className="two-buttons-wrapper">
<div className="two-item-wrapper">
<span className="two-label">A</span>
<span className="two-value">{numA}</span>
<Button variant="secondary" onClick={() => setNumA((a) => a + 1)}>
Increment A
</Button>
</div>
<div className="two-item-wrapper">
<span className="two-label">B</span>
<span className="two-value-tabular">{numB}</span>
<Button variant="secondary" onClick={() => setNumB((b) => b + 1)}>
Increment B
</Button>
</div>
</div>
</div>
);
}
If numB is removed from the array, clicking the button for B will still increment the state, but no alert notification will show up because the dependency array didn't change. As we have seen in the video at the start of the blog.
Deep Dive: The Cleanup Function
The cleanup function is critical for performance and bug prevention, as it clears previous event listeners and timers before the component re-renders or unmounts (as shown in the lifecycle diagram above).
Now let us see a timer component that tracks a seconds state. Inside useEffect, the state's value increments every second using setInterval(). In the cleanup function, the timer is cleared using clearInterval().
import { useState, useEffect } from "react";
export default function TimerUseEffect() {
const [seconds, setSeconds] = useState(0); // defined the state
useEffect(() => {
// side effect - constantly update the state each second
const intervalId = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
// clean up function - clear the interval before the next effect runs or on unmount
return () => clearInterval(intervalId);
}); // <- No dependency array given - so side effect will run on every render
return (
<div className="container">
<div className="footer-text">
Uses <code className="code">useEffect</code> to spin up and clean up an
interval timer.
</div>
<div className="iwrapper">
<span className="value">{seconds}s</span>
</div>
</div>
);
}
setInterval: Commonly used to set a delay for functions that are executed repeatedly, such as animations. Returns an integer commonly referred to as the intervalID, which can be passed to clearInterval() to stop the repeated execution. (MDN docs)
The Memory Leak Trap
Avoiding or forgetting the cleanup function in a component is a major cause of memory leaks. If we take the timer component above as an example, if we comment out the cleanup function, the counter will start increasing very fast and uncontrollably. This happens because state updates inside setInterval trigger a re-render, which in turn spawns another setInterval without clearing the previous one.

Because the old intervals are never disconnected, they stack up exponentially. Let's look at what happens in this case:
- Dide effect runs and starts a
setIntervaltimer to increment state after 1 second. - After 1 second, the state updates and triggers a re-render. Without cleanup, the old interval keeps running, while the new render's effect creates a second
setInterval. - After another second, both intervals trigger state updates, creating 2 more intervals. This causes the number of active timers to stack up exponentially.
The setInterval timers keep piling up, and in very little time the counter becomes unusable. To avoid bugs like these entirely, whenever you set up a timer or event listener in useEffect, you must clean up or disconnect it inside the cleanup function.

