Paridhi SolutionsBlog
React

React useEffect Hook Explained - Complete Beginner Guide

Learn the React useEffect Hook from scratch with practical examples. Understand side effects, lifecycle, dependency arrays, API calls, cleanup functions, timers, event listeners, local storage, best practices, interview questions, and FAQs.

Paridhi Solutions
2026-07-19
31 min read
React useEffect Hook Explained - Complete Beginner Guide

Introduction to React useEffect

As your React applications become more interactive, you'll often need to perform tasks that go beyond simply rendering UI.

For example, you might need to:

  • Fetch data from an API
  • Update the page title
  • Save user preferences
  • Listen for browser events
  • Start a timer
  • Connect to a WebSocket
  • Store data in Local Storage

These operations are known as side effects, and React provides the useEffect Hook to handle them.

If you've already learned useState, Forms, and Event Handling, then useEffect is the next essential hook to master.

In this guide, you'll learn how useEffect works, when to use it, and how to avoid common mistakes using practical, real-world examples.


What You'll Learn

By the end of this tutorial, you'll be able to:

  • Understand what side effects are
  • Use the useEffect Hook correctly
  • Run code after rendering
  • Work with dependency arrays
  • Fetch data from APIs
  • Create timers
  • Save data to Local Storage
  • Add and remove event listeners
  • Clean up resources properly
  • Avoid infinite loops
  • Follow React best practices

Let's begin by understanding what side effects actually are.


What Are Side Effects?

A side effect is any operation that interacts with something outside of the React rendering process.

Examples include:

  • Calling an API
  • Updating the browser title
  • Reading Local Storage
  • Writing Local Storage
  • Starting a timer
  • Listening for window resize events
  • Opening a WebSocket connection

These tasks don't directly generate the user interface, but they are necessary for many real-world applications.

For example:

User Opens Product Page
            │
            ▼
React Renders Component
            │
            ▼
Fetch Product Details
            │
            ▼
Receive API Response
            │
            ▼
Update State
            │
            ▼
Display Product

The API request is a side effect because it happens outside of rendering the component.


Why Do We Need useEffect?

React components should remain focused on rendering the UI.

Imagine this component:

jsx
function App() {

    document.title = "Welcome";

    return <h1>Hello React</h1>;

}

Although this works, React recommends moving side effects into useEffect.

jsx
import { useEffect } from "react";

function App() {

    useEffect(() => {

        document.title = "Welcome";

    });

    return <h1>Hello React</h1>;

}

Now React knows that updating the document title is a side effect.

This makes components easier to understand and maintain.


Understanding Component Lifecycle

Before Hooks were introduced, React developers used lifecycle methods in class components.

A component goes through three main phases:

Component Created
        │
        ▼
Mounted
        │
        ▼
Updated
        │
        ▼
Unmounted

Mount

The component appears on the screen for the first time.

Examples:

  • Home page loads
  • Product page opens
  • Dashboard appears

Update

The component re-renders because:

  • State changed
  • Props changed
  • Parent component re-rendered

Unmount

The component is removed from the page.

Examples:

  • User leaves the page
  • Modal closes
  • Component is hidden

How useEffect Replaces Lifecycle Methods

In older React class components, developers used:

  • componentDidMount()
  • componentDidUpdate()
  • componentWillUnmount()

With Hooks, all of these behaviors can be handled using useEffect.

Instead of learning multiple lifecycle methods, you only need to understand one Hook.

This simplifies React development while keeping your code more organized.


Basic Syntax of useEffect

jsx
import { useEffect } from "react";

function App() {

    useEffect(() => {

        console.log("Effect executed.");

    });

    return <h1>Hello React</h1>;

}

The syntax has two parts:

jsx
useEffect(() => {

    // Side effect code

});

The callback function contains the code you want React to execute after rendering the component.


When Does useEffect Run?

By default, useEffect runs after every render.

Example:

jsx
import { useEffect, useState } from "react";

function Counter() {

    const [count, setCount] = useState(0);

    useEffect(() => {

        console.log("Component rendered.");

    });

    return (

        <button
            onClick={() => setCount(count + 1)}
        >
            Count: {count}
        </button>

    );

}

Every time you click the button:

Button Click
      │
      ▼
State Updates
      │
      ▼
Component Re-renders
      │
      ▼
useEffect Executes

If you click five times, the message appears five times in the console.

This behavior is important to understand because many beginners accidentally trigger unnecessary effects by forgetting to control when useEffect should run.

In the next section, you'll learn how dependency arrays allow you to control exactly when an effect executes.

Understanding Dependency Arrays

One of the most important parts of the useEffect Hook is the dependency array.

The dependency array tells React when an effect should run.

jsx
useEffect(() => {

    // Side effect

}, []);

The array is always the second argument passed to useEffect.

Depending on what you put inside this array, React behaves differently.

There are three common scenarios:

  1. No dependency array
  2. Empty dependency array
  3. Dependency array with values

Let's understand each one.


1. No Dependency Array

If you don't provide a dependency array, the effect runs after every render.

jsx
import { useEffect, useState } from "react";

export default function App() {

    const [count, setCount] = useState(0);

    useEffect(() => {

        console.log("Effect executed");

    });

    return (

        <button onClick={() => setCount(count + 1)}>
            {count}
        </button>

    );

}

Console Output

Effect executed
Effect executed
Effect executed
Effect executed
...

Every state update causes:

State Changes
      │
      ▼
Component Re-renders
      │
      ▼
useEffect Executes

This is rarely what you want.


2. Empty Dependency Array

Adding an empty dependency array tells React to execute the effect only once.

jsx
useEffect(() => {

    console.log("Component Mounted");

}, []);

This is similar to the old

componentDidMount()

in class components.

Execution Flow

Page Loads
      │
      ▼
Component Mounts
      │
      ▼
useEffect Runs Once

Even if state changes later,

jsx
setCount(count + 1);

the effect will not run again.

This is commonly used for:

  • API Calls
  • Initial Data Loading
  • Authentication Check
  • Local Storage Reading
  • Initial Configuration

Example

jsx
import { useEffect } from "react";

export default function Home() {

    useEffect(() => {

        console.log("Welcome");

    }, []);

    return <h1>Home</h1>;

}

Output

Welcome

Only once.


3. Dependency Array with Variables

Sometimes you want the effect to run only when a specific value changes.

Example:

jsx
import { useState, useEffect } from "react";

export default function App() {

    const [count, setCount] = useState(0);

    useEffect(() => {

        console.log("Count changed");

    }, [count]);

    return (

        <button onClick={() => setCount(count + 1)}>
            {count}
        </button>

    );

}

Now React watches only

count

Whenever it changes,

Count Updated
      │
      ▼
useEffect Runs

Multiple Dependencies

You can also watch multiple values.

jsx
const [name, setName] = useState("");
const [age, setAge] = useState(18);

useEffect(() => {

    console.log("Data Changed");

}, [name, age]);

Now the effect runs whenever

  • name changes
  • age changes

Flow

Name Changed
        │
        ├──────────┐
        ▼          │
useEffect Runs     │
                   │
Age Changed        │
        │          │
        └──────────┘

Real Example

Imagine a shopping website.

jsx
const [category, setCategory] = useState("Mobiles");

useEffect(() => {

    fetchProducts(category);

}, [category]);

Whenever the user selects another category,

Mobiles

↓

Laptops

↓

Gaming

↓

Accessories

React automatically fetches new products.

No manual refresh required.


Dependency Arrays and Objects

One mistake beginners often make is using objects directly inside dependencies.

jsx
const user = {
    name: "John"
};

useEffect(() => {

    console.log(user);

}, [user]);

Since a new object is created on every render, React thinks the dependency has changed and runs the effect again.

This can lead to unnecessary renders or repeated API calls.

If you need stable object references, consider using useMemo or restructuring your code.


Dependency Arrays and Functions

The same issue applies to functions.

jsx
function fetchUsers() {

    console.log("Fetching...");

}

useEffect(() => {

    fetchUsers();

}, [fetchUsers]);

Since functions are recreated on every render, the effect may run more often than expected.

In larger applications, developers often use useCallback to create stable function references.

We'll cover useCallback in a dedicated tutorial later in this React series.


Summary

The dependency array determines when your effect executes.

Dependency ArrayRuns When
No arrayAfter every render
[]Only once after the component mounts
[count]When count changes
[name, age]When name or age changes

Understanding dependency arrays is the key to using useEffect effectively.

In the next section, we'll explore cleanup functions, where you'll learn how to stop timers, remove event listeners, and prevent memory leaks when components are removed from the page.

Cleanup Functions in useEffect

So far, we've learned how to execute code using useEffect.

However, some side effects continue running even after a component disappears from the screen.

Examples include:

  • Timers
  • Event listeners
  • WebSocket connections
  • API subscriptions
  • Intervals

If these aren't cleaned up properly, they can cause:

  • Memory leaks
  • Unexpected behavior
  • Duplicate event listeners
  • Poor performance

React solves this problem using cleanup functions.


What is a Cleanup Function?

A cleanup function is a function returned from inside useEffect.

jsx
useEffect(() => {

    // Effect

    return () => {

        // Cleanup

    };

}, []);

When React removes the component from the page, it automatically executes the cleanup function.

Think of it like this:

Component Mounts
        │
        ▼
Run Effect
        │
        ▼
Component Active
        │
        ▼
Component Unmounts
        │
        ▼
Cleanup Function Executes

Why Cleanup is Important

Imagine starting a timer.

jsx
useEffect(() => {

    setInterval(() => {

        console.log("Running");

    }, 1000);

}, []);

Now suppose the user leaves the page.

Without cleanup:

User Leaves Page
        │
        ▼
Timer Keeps Running ❌

Every time the user returns to the page, another timer starts.

Eventually you may have:

Timer 1

Timer 2

Timer 3

Timer 4

all running together.

This wastes memory and slows down the application.


Cleaning Up Timers

The correct approach is:

jsx
import { useEffect } from "react";

export default function Timer() {

    useEffect(() => {

        const timer = setInterval(() => {

            console.log("Running...");

        }, 1000);

        return () => {

            clearInterval(timer);

        };

    }, []);

    return <h1>Timer Example</h1>;

}

Now the sequence becomes:

Component Mounts
        │
        ▼
Start Timer
        │
        ▼
User Leaves Page
        │
        ▼
clearInterval()
        │
        ▼
Timer Stops ✅

Cleanup with setTimeout()

The same principle applies to setTimeout().

Incorrect:

jsx
useEffect(() => {

    setTimeout(() => {

        console.log("Hello");

    }, 5000);

}, []);

Correct:

jsx
useEffect(() => {

    const timeout = setTimeout(() => {

        console.log("Hello");

    }, 5000);

    return () => {

        clearTimeout(timeout);

    };

}, []);

Removing Event Listeners

Suppose you want to detect browser resizing.

jsx
useEffect(() => {

    function handleResize() {

        console.log(window.innerWidth);

    }

    window.addEventListener(
        "resize",
        handleResize
    );

}, []);

Looks fine...

Until the component mounts again.

Now another listener is added.

Resize

↓

Listener 1

↓

Listener 2

↓

Listener 3

Eventually every resize executes multiple listeners.


Correct Version

jsx
useEffect(() => {

    function handleResize() {

        console.log(window.innerWidth);

    }

    window.addEventListener(
        "resize",
        handleResize
    );

    return () => {

        window.removeEventListener(
            "resize",
            handleResize
        );

    };

}, []);

Now React removes the listener automatically.


Real Example: Window Width Tracker

jsx
import { useEffect, useState } from "react";

export default function WindowWidth() {

    const [width, setWidth] =
        useState(window.innerWidth);

    useEffect(() => {

        function updateWidth() {

            setWidth(window.innerWidth);

        }

        window.addEventListener(
            "resize",
            updateWidth
        );

        return () => {

            window.removeEventListener(
                "resize",
                updateWidth
            );

        };

    }, []);

    return <h2>Width: {width}px</h2>;

}

Now the width updates whenever the browser is resized, and React removes the listener when the component unmounts.


Cleaning Up API Subscriptions

Some APIs maintain long-lived connections.

Examples include:

  • WebSocket
  • Firebase
  • Supabase Realtime
  • Socket.IO
  • Chat applications

Example:

jsx
useEffect(() => {

    const socket = connect();

    return () => {

        socket.disconnect();

    };

}, []);

When the user leaves the page:

Disconnect Socket

↓

Release Memory

↓

Close Connection

Without cleanup, the connection would remain open.


When Does Cleanup Run?

A common misconception is that cleanup only runs when a component unmounts.

Actually, cleanup runs in two situations:

1. Before the effect runs again

If an effect depends on a value:

jsx
useEffect(() => {

    console.log("Effect");

    return () => {

        console.log("Cleanup");

    };

}, [count]);

Every time count changes:

Cleanup Previous Effect

↓

Run New Effect

This ensures React doesn't leave outdated effects running.


2. When the component unmounts

If the component is removed from the page:

Component Removed

↓

Cleanup Executes

↓

Resources Released

Common Mistakes

Forgetting Cleanup

jsx
window.addEventListener(
    "resize",
    handleResize
);

Every render adds another listener.


Correct

jsx
return () => {

    window.removeEventListener(
        "resize",
        handleResize
    );

};

Forgetting clearInterval()

jsx
setInterval(...);

The timer continues forever.


Correct

jsx
const timer = setInterval(...);

return () => {

    clearInterval(timer);

};

Best Practices

  • Always clean up timers.
  • Remove event listeners when they're no longer needed.
  • Disconnect WebSockets and subscriptions.
  • Avoid creating duplicate listeners.
  • Remember that cleanup also runs before an effect re-executes because its dependencies changed.

Following these practices helps prevent memory leaks and keeps your React applications fast and reliable.


Summary

Cleanup functions are an essential part of useEffect. They allow you to stop timers, remove event listeners, close connections, and release resources when they're no longer needed.

Without proper cleanup, your application can accumulate unused timers, duplicate listeners, and open connections, leading to bugs and performance issues.

In the next section, we'll explore one of the most common uses of useEffect: fetching data from APIs, including loading states, error handling, and displaying data in your React components.

Fetching Data from APIs with useEffect

One of the most common uses of the useEffect Hook is fetching data from an API.

Modern web applications rarely contain all of their data inside the frontend. Instead, they retrieve information from backend services.

Some examples include:

  • Loading products from an e-commerce API
  • Fetching blog posts
  • Displaying user profiles
  • Showing weather information
  • Loading order history
  • Retrieving dashboard statistics

The typical flow looks like this:

Component Loads
        │
        ▼
useEffect Executes
        │
        ▼
API Request Sent
        │
        ▼
Server Responds
        │
        ▼
Update State
        │
        ▼
React Re-renders UI

Basic API Example

Let's start with a simple example using the Fetch API.

jsx
import { useEffect, useState } from "react";

export default function Users() {

    const [users, setUsers] = useState([]);

    useEffect(() => {

        fetch("https://jsonplaceholder.typicode.com/users")
            .then((response) => response.json())
            .then((data) => {

                setUsers(data);

            });

    }, []);

    return (

        <ul>

            {users.map((user) => (

                <li key={user.id}>

                    {user.name}

                </li>

            ))}

        </ul>

    );

}

Since the dependency array is empty ([]), the API request is made only once when the component mounts.


Why Use an Empty Dependency Array?

Imagine if you removed the dependency array.

jsx
useEffect(() => {

    fetchUsers();

});

Now every render triggers another API call.

Render

↓

API Request

↓

State Updates

↓

Render Again

↓

API Request Again

↓

Render Again

↓

API Request Again

...

This creates an infinite loop of requests.

Using an empty dependency array prevents that behavior.

jsx
useEffect(() => {

    fetchUsers();

}, []);

Now the request runs only once.


Using async/await

Many developers prefer async/await because it is easier to read.

jsx
useEffect(() => {

    async function fetchUsers() {

        const response = await fetch(
            "https://jsonplaceholder.typicode.com/users"
        );

        const data = await response.json();

        setUsers(data);

    }

    fetchUsers();

}, []);

Notice that the async function is declared inside useEffect.

You shouldn't make the effect callback itself async.

❌ Incorrect

jsx
useEffect(async () => {

}, []);

React expects the callback to either return nothing or return a cleanup function, not a Promise.


Showing a Loading State

API requests are not instant.

Users should know that data is loading.

jsx
const [loading, setLoading] =
    useState(true);

Inside the request:

jsx
async function fetchUsers() {

    const response = await fetch(...);

    const data = await response.json();

    setUsers(data);

    setLoading(false);

}

Display the loading message.

jsx
if (loading) {

    return <h2>Loading...</h2>;

}

Flow:

Page Opens

↓

Loading...

↓

API Response

↓

Display Users

A loading indicator greatly improves the user experience.


Handling API Errors

Sometimes requests fail because of:

  • Network issues
  • Server errors
  • Invalid endpoints
  • Authentication failures

Always handle errors gracefully.

jsx
const [error, setError] =
    useState("");

Example:

jsx
async function fetchUsers() {

    try {

        const response =
            await fetch(
                "https://jsonplaceholder.typicode.com/users"
            );

        const data =
            await response.json();

        setUsers(data);

    } catch (err) {

        setError(
            "Unable to load users."
        );

    } finally {

        setLoading(false);

    }

}

Display the error.

jsx
if (error) {

    return <h2>{error}</h2>;

}

Users receive helpful feedback instead of seeing a blank page.


Complete Example

jsx
import { useEffect, useState } from "react";

export default function Users() {

    const [users, setUsers] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState("");

    useEffect(() => {

        async function fetchUsers() {

            try {

                const response =
                    await fetch(
                        "https://jsonplaceholder.typicode.com/users"
                    );

                if (!response.ok) {

                    throw new Error(
                        "Request failed."
                    );

                }

                const data =
                    await response.json();

                setUsers(data);

            } catch (err) {

                setError(
                    "Failed to fetch users."
                );

            } finally {

                setLoading(false);

            }

        }

        fetchUsers();

    }, []);

    if (loading) {

        return <h2>Loading...</h2>;

    }

    if (error) {

        return <h2>{error}</h2>;

    }

    return (

        <ul>

            {users.map((user) => (

                <li key={user.id}>

                    {user.name}

                </li>

            ))}

        </ul>

    );

}

Fetching Data When a Dependency Changes

Sometimes data should reload whenever a value changes.

Imagine an online store where users choose a product category.

jsx
const [category, setCategory] =
    useState("electronics");

Now fetch products whenever the category changes.

jsx
useEffect(() => {

    async function fetchProducts() {

        const response = await fetch(

            `/api/products?category=${category}`

        );

        const data =
            await response.json();

        setProducts(data);

    }

    fetchProducts();

}, [category]);

Flow:

Electronics

↓

Books

↓

Fashion

↓

Accessories

Each time the category changes, React automatically fetches fresh data.


Best Practices for API Calls

When fetching data with useEffect, keep these tips in mind:

  • Use an empty dependency array for initial data loading.
  • Display a loading state while waiting for the response.
  • Handle errors gracefully.
  • Avoid making the effect callback async.
  • Validate the server response before using the data.
  • Keep API logic inside a separate function for better readability.
  • Consider moving reusable API logic into a custom Hook as your application grows.

Summary

Fetching data from APIs is one of the most common reasons to use the useEffect Hook. By combining useEffect with state management, loading indicators, and proper error handling, you can build responsive and user-friendly applications.

In the next section, we'll explore more practical examples of useEffect, including Local Storage, Dark Mode, Timers, and Window Event Listeners, which are frequently used in real-world React projects.

Real-World useEffect Examples

So far, you've learned the fundamentals of useEffect, dependency arrays, cleanup functions, and API calls.

Now let's explore some practical examples that you'll frequently encounter in real React applications.

These examples demonstrate how useEffect interacts with the browser and external resources.


Example 1: Updating the Page Title

A common use case is changing the browser tab title dynamically.

jsx
import { useState, useEffect } from "react";

export default function Counter() {

    const [count, setCount] = useState(0);

    useEffect(() => {

        document.title = `Clicked ${count} times`;

    }, [count]);

    return (

        <button
            onClick={() => setCount(count + 1)}
        >
            Clicked {count}
        </button>

    );

}

Every click updates:

Clicked 0 times

↓

Clicked 1 times

↓

Clicked 2 times

This technique is useful for:

  • Notifications
  • Shopping cart item count
  • Dashboard updates
  • Active chat messages

Example 2: Saving Data to Local Storage

Users expect applications to remember their preferences.

For example:

  • Dark Mode
  • Language
  • Theme
  • Username
  • Shopping Cart

React can automatically save values whenever they change.

jsx
import { useState, useEffect } from "react";

export default function App() {

    const [username, setUsername] =
        useState("");

    useEffect(() => {

        localStorage.setItem(
            "username",
            username
        );

    }, [username]);

    return (

        <input
            value={username}
            onChange={(e) =>
                setUsername(e.target.value)
            }
        />

    );

}

Flow

User Types

↓

State Updates

↓

useEffect Runs

↓

Save to Local Storage

Reading from Local Storage

Saving data isn't enough.

When the application loads, we also want to restore it.

jsx
useEffect(() => {

    const savedName =
        localStorage.getItem("username");

    if (savedName) {

        setUsername(savedName);

    }

}, []);

Now users won't lose their data after refreshing the page.


Example 3: Dark Mode

Many modern applications allow users to switch between light and dark themes.

jsx
const [darkMode, setDarkMode] =
    useState(false);

useEffect(() => {

    document.body.classList.toggle(
        "dark",
        darkMode
    );

}, [darkMode]);

Whenever the user changes the theme:

Light

↓

Dark

↓

Light

React automatically updates the page.


Example 4: Auto Save

Imagine writing a blog post.

Instead of waiting until the user clicks Save, React can automatically save changes.

jsx
const [content, setContent] =
    useState("");

useEffect(() => {

    localStorage.setItem(
        "draft",
        content
    );

}, [content]);

Every keystroke updates the saved draft.

Applications like:

  • Google Docs
  • Notion
  • Medium

use similar techniques.


Example 5: Digital Clock

A digital clock updates every second.

jsx
import { useEffect, useState } from "react";

export default function Clock() {

    const [time, setTime] =
        useState(new Date());

    useEffect(() => {

        const timer =
            setInterval(() => {

                setTime(new Date());

            }, 1000);

        return () => {

            clearInterval(timer);

        };

    }, []);

    return (

        <h2>

            {time.toLocaleTimeString()}

        </h2>

    );

}

This combines:

  • State
  • useEffect
  • Cleanup Function
  • Timer

Example 6: Countdown Timer

jsx
const [seconds, setSeconds] =
    useState(10);

useEffect(() => {

    if (seconds === 0) return;

    const timer = setTimeout(() => {

        setSeconds(seconds - 1);

    }, 1000);

    return () => {

        clearTimeout(timer);

    };

}, [seconds]);

Output

10

↓

9

↓

8

↓

...

↓

0

Perfect for:

  • OTP verification
  • Quiz timers
  • Flash sales
  • Online exams

Example 7: Scroll Position Tracker

Sometimes applications need to know how far a user has scrolled.

jsx
import { useEffect, useState } from "react";

export default function ScrollTracker() {

    const [scrollY, setScrollY] =
        useState(0);

    useEffect(() => {

        function handleScroll() {

            setScrollY(window.scrollY);

        }

        window.addEventListener(
            "scroll",
            handleScroll
        );

        return () => {

            window.removeEventListener(
                "scroll",
                handleScroll
            );

        };

    }, []);

    return <h2>{scrollY}px</h2>;

}

Applications use this for:

  • Sticky navigation bars
  • Reading progress indicators
  • Infinite scrolling
  • Back-to-top buttons

Example 8: Online / Offline Status

You can even detect whether a user is connected to the internet.

jsx
import { useEffect, useState } from "react";

export default function NetworkStatus() {

    const [online, setOnline] =
        useState(navigator.onLine);

    useEffect(() => {

        function goOnline() {

            setOnline(true);

        }

        function goOffline() {

            setOnline(false);

        }

        window.addEventListener(
            "online",
            goOnline
        );

        window.addEventListener(
            "offline",
            goOffline
        );

        return () => {

            window.removeEventListener(
                "online",
                goOnline
            );

            window.removeEventListener(
                "offline",
                goOffline
            );

        };

    }, []);

    return (

        <h2>

            {online
                ? "Online ✅"
                : "Offline ❌"}

        </h2>

    );

}

This is especially useful for:

  • Chat applications
  • Banking apps
  • E-commerce websites
  • Offline-first applications

Summary

These examples demonstrate how versatile the useEffect Hook is. Whether you're updating the page title, saving user preferences, managing timers, tracking browser events, or responding to network changes, useEffect provides a consistent way to synchronize your React components with the outside world.

As you build more complex applications, you'll find yourself using these patterns repeatedly.

In the next section, we'll look at the most common mistakes developers make with useEffect, explain why they happen, and show you how to avoid them.

Common Mistakes with useEffect

The useEffect Hook is powerful, but it's also one of the most misunderstood parts of React.

Many bugs such as:

  • Infinite API requests
  • Slow performance
  • Memory leaks
  • Unexpected renders
  • Missing updates

are caused by using useEffect incorrectly.

Let's look at the most common mistakes and how to avoid them.


Mistake 1: Forgetting the Dependency Array

One of the most common beginner mistakes is omitting the dependency array.

❌ Incorrect

jsx
useEffect(() => {

    fetchProducts();

});

This runs after every render.

Flow:

Render

↓

API Call

↓

State Updates

↓

Render

↓

API Call

↓

Render

↓

API Call

Eventually your application continuously sends requests.


Correct

jsx
useEffect(() => {

    fetchProducts();

}, []);

Now the API is called only once.


Mistake 2: Creating Infinite Loops

Another common mistake is updating the same state that the effect depends on.

jsx
const [count, setCount] =
    useState(0);

useEffect(() => {

    setCount(count + 1);

}, [count]);

Execution

Count Changes

↓

Effect Runs

↓

Count Changes

↓

Effect Runs

↓

Forever...

React keeps rendering until it throws:

Maximum update depth exceeded

Correct

Only update state when necessary.

jsx
useEffect(() => {

    console.log(count);

}, [count]);

Or move the state update to an event handler.


Mistake 3: Missing Dependencies

Sometimes developers intentionally leave dependencies out.

jsx
useEffect(() => {

    console.log(name);

}, []);

If name changes later,

the effect never runs again.

React even warns you:

React Hook useEffect has a missing dependency.

Correct

jsx
useEffect(() => {

    console.log(name);

}, [name]);

Now the effect always uses the latest value.


Mistake 4: Ignoring ESLint Warnings

Many developers see this warning:

React Hook has missing dependencies.

Instead of fixing it,

they disable ESLint.

jsx
// eslint-disable-next-line

This hides the warning but doesn't solve the problem.

Always understand why React suggests adding a dependency before suppressing the warning.


Mistake 5: Making useEffect async

This is another common error.

jsx
useEffect(async () => {

    const response =
        await fetch("/api");

}, []);

React expects the callback to return either:

  • nothing
  • a cleanup function

An async function returns a Promise instead.


Correct

jsx
useEffect(() => {

    async function loadData() {

        const response =
            await fetch("/api");

        const data =
            await response.json();

    }

    loadData();

}, []);

Mistake 6: Forgetting Cleanup

Consider this code.

jsx
useEffect(() => {

    window.addEventListener(
        "resize",
        handleResize
    );

}, []);

Every time the component mounts,

another listener is added.

Eventually:

Resize

↓

Listener 1

↓

Listener 2

↓

Listener 3

Multiple listeners execute simultaneously.


Correct

jsx
useEffect(() => {

    window.addEventListener(
        "resize",
        handleResize
    );

    return () => {

        window.removeEventListener(
            "resize",
            handleResize
        );

    };

}, []);

Mistake 7: Using Objects as Dependencies

Consider this example.

jsx
const user = {

    name: "John"

};

useEffect(() => {

    console.log(user);

}, [user]);

Looks fine...

But React creates a new object every render.

Render

↓

New Object

↓

Dependency Changed

↓

Effect Runs

Even if nothing actually changed.


Better Approach

Store objects in state or memoize them.

jsx
const user =
    useMemo(() => ({

        name: "John"

    }), []);

This keeps the object reference stable.


Mistake 8: Using Functions as Dependencies

Functions behave similarly.

jsx
function loadUsers() {

}

Every render creates a new function.

React sees:

New Function

↓

Dependency Changed

↓

Run Effect

Better Solution

Use useCallback.

jsx
const loadUsers =
    useCallback(() => {

        // Fetch users

    }, []);

We'll learn useCallback in a later article.


Mistake 9: Putting Everything Inside One Effect

Some beginners write enormous effects.

jsx
useEffect(() => {

    fetchProducts();

    document.title = "Shop";

    window.addEventListener(...);

    localStorage.setItem(...);

}, []);

This becomes difficult to read and maintain.


Better Practice

Split unrelated logic.

jsx
useEffect(() => {

    fetchProducts();

}, []);

useEffect(() => {

    document.title = "Shop";

}, []);

useEffect(() => {

    localStorage.setItem(
        "theme",
        theme
    );

}, [theme]);

Each effect has one responsibility.


Mistake 10: Using useEffect When You Don't Need It

Not every piece of logic belongs in useEffect.

For example:

jsx
const fullName =
    firstName + " " + lastName;

useEffect(() => {

    setName(fullName);

}, [firstName, lastName]);

This creates unnecessary state and an extra render.


Better

Simply calculate the value during rendering.

jsx
const fullName =
    `${firstName} ${lastName}`;

No useEffect required.

A good rule is:

If a value can be calculated from existing props or state during rendering, you usually don't need an effect.


Best Practices

Follow these recommendations whenever you write an effect:

  • Keep each effect focused on a single task.
  • Always specify dependencies correctly.
  • Never ignore ESLint warnings without understanding them.
  • Clean up timers, event listeners, and subscriptions.
  • Avoid storing derived values in state.
  • Use useMemo and useCallback only when they solve a real problem.
  • Use useEffect for synchronizing with external systems, not for ordinary calculations.

Quick Checklist

Before writing a useEffect, ask yourself:

✅ Does this interact with something outside React?

✅ Does it fetch data?

✅ Does it subscribe to an event?

✅ Does it start a timer?

✅ Does it update the browser?

If the answer is yes, useEffect is probably appropriate.

If the logic simply derives one value from another, you likely don't need an effect.


Summary

Understanding these common mistakes will help you write cleaner and more efficient React applications. Most issues with useEffect come from incorrect dependencies, missing cleanup, or using it for work that doesn't belong in an effect.

By keeping your effects focused, cleaning up resources, and respecting dependency arrays, you'll avoid many of the bugs that frustrate new React developers.

In the next section, we'll wrap up this guide with Frequently Asked Questions (FAQs), Interview Questions, a quick recap, and key takeaways to reinforce everything you've learned.

Frequently Asked Questions (FAQs)

1. What is the useEffect Hook?

The useEffect Hook allows you to perform side effects in React components.

Common side effects include:

  • Fetching data from an API
  • Updating the browser title
  • Reading or writing Local Storage
  • Starting timers
  • Adding event listeners
  • Connecting to WebSockets

2. When does useEffect run?

It depends on the dependency array.

jsx
useEffect(() => {

    console.log("Runs after every render");

});

Runs after every render.

jsx
useEffect(() => {

    console.log("Runs once");

}, []);

Runs only once after the component mounts.

jsx
useEffect(() => {

    console.log("Runs when count changes");

}, [count]);

Runs whenever count changes.


3. Why do we use the dependency array?

The dependency array tells React when an effect should execute.

Without it:

Every Render

With it:

Only when dependencies change

This improves performance and prevents unnecessary work.


4. Can I have multiple useEffect Hooks?

Yes.

In fact, it's recommended.

Instead of writing one large effect,

write several smaller ones.

Example:

jsx
useEffect(() => {

    fetchProducts();

}, []);

useEffect(() => {

    document.title = title;

}, [title]);

useEffect(() => {

    localStorage.setItem(
        "theme",
        theme
    );

}, [theme]);

Each effect has a single responsibility.


5. Can I use async directly in useEffect?

No.

Incorrect:

jsx
useEffect(async () => {

}, []);

Correct:

jsx
useEffect(() => {

    async function loadData() {

    }

    loadData();

}, []);

6. Why does React ask for cleanup functions?

Cleanup functions help React remove resources that are no longer needed.

Examples include:

  • Timers
  • Event listeners
  • WebSockets
  • Subscriptions

Without cleanup, your application may suffer from memory leaks or duplicate listeners.


7. Does useEffect replace lifecycle methods?

Yes.

In class components, developers used:

  • componentDidMount()
  • componentDidUpdate()
  • componentWillUnmount()

With Hooks, useEffect covers these scenarios depending on how you configure the dependency array and cleanup function.


8. Can I fetch APIs using useEffect?

Absolutely.

This is one of its most common use cases.

jsx
useEffect(() => {

    async function fetchUsers() {

        const response =
            await fetch("/api/users");

        const data =
            await response.json();

        setUsers(data);

    }

    fetchUsers();

}, []);

9. What happens if I forget the dependency array?

The effect runs after every render.

This may result in:

  • Repeated API requests
  • Performance issues
  • Infinite loops
  • Unnecessary computations

Always think carefully about when an effect should run.


10. When should I avoid useEffect?

Avoid using useEffect for values that can be computed directly during rendering.

For example:

jsx
const fullName =
    `${firstName} ${lastName}`;

There's no need to store fullName in state or update it with an effect.


React useEffect Interview Questions

Here are some common interview questions to test your understanding of the useEffect Hook.


Beginner Level

1. What is the purpose of the useEffect Hook?

Answer:

It allows React components to perform side effects such as API calls, timers, event listeners, and updating browser APIs after rendering.


2. What are side effects?

Answer:

Side effects are operations that interact with systems outside React's rendering process, such as fetching data, accessing Local Storage, or subscribing to browser events.


3. What is the dependency array?

Answer:

The dependency array tells React when an effect should run.

Examples:

  • No array → after every render
  • [] → once after mount
  • [count] → whenever count changes

4. Why shouldn't you make the useEffect callback async?

Answer:

An async function returns a Promise, but React expects the callback to return either nothing or a cleanup function.

Instead, define an async function inside the effect and call it.


5. Why are cleanup functions important?

Answer:

Cleanup functions prevent memory leaks by removing timers, event listeners, subscriptions, and other resources when they're no longer needed.


Intermediate Level

6. What causes an infinite loop in useEffect?

Answer:

Updating state inside an effect while also depending on that same state.

Example:

jsx
useEffect(() => {

    setCount(count + 1);

}, [count]);

7. What happens if you omit a dependency?

Answer:

The effect may use outdated values because it won't re-run when that dependency changes. React's Hooks ESLint plugin will typically warn about this.


8. Why should unrelated logic be split into multiple useEffect Hooks?

Answer:

Smaller effects are easier to read, test, maintain, and reason about. Each effect should ideally synchronize one concern.


9. When does the cleanup function execute?

Answer:

A cleanup function runs:

  • Before the effect re-runs due to dependency changes.
  • When the component unmounts.

10. When should you use useEffect?

Answer:

Use useEffect whenever your component needs to synchronize with something outside React, such as:

  • APIs
  • Browser APIs
  • Timers
  • Event listeners
  • Local Storage
  • WebSocket connections

Summary

Congratulations! 🎉

You've now learned one of the most important Hooks in React.

Throughout this guide, you explored:

  • What side effects are
  • Why useEffect exists
  • Component lifecycle concepts
  • Dependency arrays
  • Cleanup functions
  • Fetching API data
  • Local Storage
  • Timers
  • Event listeners
  • Real-world examples
  • Common mistakes
  • Best practices
  • Frequently asked questions
  • Interview questions

The useEffect Hook helps your React components stay synchronized with the outside world while keeping rendering predictable and efficient.

As you build larger applications, you'll use useEffect for tasks such as authentication, API integration, analytics, notifications, and browser interactions. Understanding when—and just as importantly, when not—to use it is a key step toward writing clean React applications.


Key Takeaways

  • useEffect is used for side effects.
  • The dependency array controls when an effect runs.
  • Use [] to run an effect only once after the component mounts.
  • Always clean up timers, subscriptions, and event listeners.
  • Avoid updating state in a way that creates infinite loops.
  • Don't make the effect callback async; create an async function inside it instead.
  • Split unrelated logic into multiple effects.
  • Don't use useEffect for values that can be derived during rendering.

What's Next?

Now that you've mastered useEffect, it's time to learn how to share state across multiple components without prop drilling.

In the next tutorial, we'll cover React Context API Explained, where you'll learn:

  • What the Context API is
  • Why prop drilling is a problem
  • Creating and providing context
  • Consuming context with useContext
  • Managing global themes and user authentication
  • Best practices and common pitfalls
  • Real-world examples and interview questions

By the end of that guide, you'll be able to build cleaner, more scalable React applications with shared state.

Tags

ReactuseEffectReact HooksAPIReact Tutorial

Written by

Paridhi Solutions

Publishing in-depth tutorials, coding guides, and best practices for modern web development.

Visit Website →

Share this article

Related Articles