Master Code
On The Go

Learn. Practice. Build.

Home › How-To › React useEffect Hook

React Guide

React useEffect Hook

The hook that handles side effects — fetching, subscriptions, timers, and cleanup.

Last updated: October 2026 · Tested on React 18+

Quick answer: useEffect runs side effects after render. The dependency array controls when: [] runs once on mount, no array runs every render, and [value] runs when value changes. Return a cleanup function to cancel subscriptions, abort requests, or clear timers.

1

Basic Syntax

import { useState, useEffect } from 'react';

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

    useEffect(() => {
        document.title = `Count: ${count}`;
    }, [count]);

    return (
        <button onClick={() => setCount(c => c + 1)}>
            Clicked {count} times
        </button>
    );
}

Every time count changes, the effect runs and updates the document title.

2

The Dependency Array Explained

Dependency When the effect runs
useEffect(fn) After every render (rare, usually a bug)
useEffect(fn, []) Once on mount
useEffect(fn, [a, b]) When a or b changes

Rule: Every value used inside the effect that comes from state, props, or context must be in the dependency array. ESLint's react-hooks/exhaustive-deps rule enforces this — enable it.

3

Fetching Data (The Classic Use Case)

function UserProfile({ userId }) {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        const controller = new AbortController();
        setLoading(true);

        fetch(`/api/users/${userId}`, { signal: controller.signal })
            .then(res => res.json())
            .then(data => {
                setUser(data);
                setLoading(false);
            })
            .catch(err => {
                if (err.name !== 'AbortError') {
                    setError(err);
                    setLoading(false);
                }
            });

        return () => controller.abort();
    }, [userId]);

    if (loading) return <p>Loading...</p>;
    if (error) return <p>Error: {error.message}</p>;
    return <h1>{user.name}</h1>;
}

The AbortController cancels the fetch on cleanup. Without it, a user switching profiles quickly would set state on an unmounted component — a bug that used to be very common.

4

The Cleanup Function

Return a function from useEffect to clean up:

// Timers
useEffect(() => {
    const id = setInterval(() => console.log('tick'), 1000);
    return () => clearInterval(id);
}, []);

// Event listeners
useEffect(() => {
    const handleScroll = () => console.log(window.scrollY);
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
}, []);

// WebSocket connections
useEffect(() => {
    const ws = new WebSocket('wss://api.example.com');
    ws.onmessage = (e) => console.log(e.data);
    return () => ws.close();
}, []);

Every subscription needs a matching cleanup. Memory leaks and "cannot read property of undefined" bugs usually trace back to missing cleanup.

5

Async Functions in useEffect

You cannot pass an async function directly to useEffect — it returns a Promise, not a cleanup function. Wrap it:

// ❌ Wrong
useEffect(async () => {
    const data = await fetchData();
    setData(data);
}, []);

// ✅ Correct
useEffect(() => {
    async function load() {
        const data = await fetchData();
        setData(data);
    }
    load();
}, []);

// ✅ Also correct (with IIFE)
useEffect(() => {
    (async () => {
        const data = await fetchData();
        setData(data);
    })();
}, []);
6

Avoiding Infinite Loops

An infinite loop happens when the effect updates a value that is in its own dependency array:

// ❌ Infinite loop — runs on every render
useEffect(() => {
    setCount(count + 1);
}, [count]);

// ❌ Also common — object created every render
useEffect(() => {
    // ...
}, [{ id: 1 }]); // New object every render → effect runs every render

// ✅ Stable value
const id = 1;
useEffect(() => {
    // ...
}, [id]);

💡 Fix: If you need a stable reference to an object, wrap it in useMemo. For functions, wrap them in useCallback.

🎯 When NOT to Use useEffect

The modern rule: You probably don't need useEffect. Reach for it only when you genuinely need to synchronize React state with an external system — a subscription, a browser API, a timer, or a network request.

❓ Frequently Asked Questions

What does useEffect do in React?

useEffect lets you run side effects in a functional component — like fetching data, updating the DOM, setting up subscriptions, or timers. It replaces lifecycle methods like componentDidMount and componentDidUpdate.

What is the dependency array in useEffect?

The dependency array controls when the effect runs. Empty [] means run once on mount. Missing array means run on every render. Values in the array mean run when any of those values change.

How do I fetch data in useEffect?

Use useEffect with an empty dependency array and an async function inside. Handle loading and error states. Always cancel the request on cleanup to avoid setting state on an unmounted component.

Why is my useEffect running twice in development?

React 18 Strict Mode intentionally runs effects twice in development to help you find bugs. It does not happen in production. Write effects so they are safe to run twice (idempotent).

What is the cleanup function in useEffect?

The cleanup function is returned from useEffect and runs before the component unmounts or before the effect runs again. Use it to cancel subscriptions, clear timers, and abort fetch requests.

🎯 What's Next?

← All How-To Guides