useEffect Hook - React Fundamentals

useEffect Hook - React Fundamentals

Nikhil Nath R Sep 10, 2026

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:

jsx
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 Mode is ON during 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:

jsx
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().

jsx
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:

  1. Dide effect runs and starts a setInterval timer to increment state after 1 second.
  2. 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.
  3. 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.

Ready to Learn With ArtUs Academy?

Full Stack Web Development
Build full production-ready applications
Professional Digital Marketing Course
Master ads, SEO, content strategy and analytics
Video Editing Foundations
Master the art of cutting, motion graphics, and delivery