HooksIntermediate10 min08 / 9

The useEffect Hook

Learn how useEffect lets your React components reach outside themselves — fetching data, setting up timers, and cleaning up after themselves when they're done.

React components have one job: take some data and return some UI. But real apps need to do more than that. They fetch data from a server. They set up a timer that ticks every second. They subscribe to a WebSocket that streams live updates. They update the browser tab's title.

None of these things fit neatly inside a return statement — they reach outside the component into the world beyond React. We call these side effects, and useEffect is the hook React gives you to manage them safely.

#What Is a Side Effect?

Think of it like

A Chef Who Also Texts the Table

Imagine a chef whose job is to cook a dish (return UI). But sometimes the chef also needs to do something outside the kitchen — text the table that the food is ready, order more ingredients, or turn off the stove after a timer.

Those extra tasks are side effects: they happen as a consequence of cooking, but they aren't the dish itself. useEffect is React's way of saying: "after you've cooked and served the dish, here's where you handle those extra tasks."

Common side effects in React include: - Fetching data from an API after the component appears on screen - Setting up a timer with setInterval or setTimeout - Subscribing to an event or a WebSocket connection - Updating the document title shown in the browser tab - Reading from localStorage

All of these are jobs for useEffect.

#The Basic Shape of useEffect

useEffect takes two arguments: a callback function where your side effect lives, and an optional dependency array that tells React when to re-run it.

Without a dependency array, this effect runs after every single render.
import { useState, useEffect } from 'react';

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <button onClick={() => setCount(count + 1)}>
      Click me ({count})
    </button>
  );
}

React runs the callback after the component renders and the DOM has updated. That timing is intentional — your effect always sees the most up-to-date values on screen.

#The Dependency Array: Controlling When Effects Run

Running an effect after every render is often too much. If you're fetching data, you don't want to hit the network on every keystroke. The dependency array (the second argument) is how you tell React when to re-run the effect.

There are three cases:

Three dependency array patterns and what they mean.
// 1. No array — runs after EVERY render
useEffect(() => {
  console.log('Runs every time');
});

// 2. Empty array — runs ONCE after the first render
useEffect(() => {
  console.log('Runs once on mount');
}, []);

// 3. Array with values — runs when those values change
useEffect(() => {
  console.log('userId changed:', userId);
}, [userId]);

The empty array [] is your go-to for data fetching — you want to load data once when the component first appears, not on every update. The array with values is perfect for reacting to specific changes, like re-fetching a user's profile whenever their userId changes.

#Fetching Data with useEffect

Here's a practical, complete example: fetching a user from an API when the component mounts.

Fetch on mount and re-fetch whenever userId changes.
import { useState, useEffect } from 'react';

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

  useEffect(() => {
    setLoading(true);
    fetch(`https://api.example.com/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });
  }, [userId]);

  if (loading) return <p>Loading...</p>;
  return <h2>Hello, {user.name}!</h2>;
}

#The Cleanup Function

Some effects need to be undone when the component leaves the screen (unmounts) or before the effect runs again. A timer needs to be cleared. A subscription needs to be cancelled. An event listener needs to be removed.

You handle this by returning a function from your effect — React calls it at the right moment automatically.

Always return a cleanup function when you create a timer or subscription.
import { useState, useEffect } from 'react';

function Clock() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    // Cleanup: stop the timer when component unmounts
    return () => clearInterval(interval);
  }, []); // Empty array: set up once, clean up once

  return <p>Elapsed: {seconds}s</p>;
}
Tip

When Does Cleanup Run?

React calls your cleanup function in two situations: 1. Before the effect runs again — so the old setup is torn down before the new one starts. 2. When the component unmounts — so you don't leave timers or listeners running in the background.

Think of it as always tidying up before leaving the kitchen.

#Common Pitfalls

Common mistake

Infinite Loops: The Most Common useEffect Mistake

If you set state inside an effect without a dependency array (or with a dependency that changes every render), you'll trigger an infinite loop:

``jsx // DANGER: infinite loop! useEffect(() => { setCount(count + 1); // triggers re-render }); // runs again after re-render... forever ``

The fix is almost always adding a proper dependency array. If the effect should run once, use []. If it should run when specific values change, list exactly those values.

Watch out

Missing Dependencies

If your effect uses a variable but you don't include it in the dependency array, your effect will run with a stale (out-of-date) copy of that value. This is called a stale closure and causes subtle bugs.

If you have ESLint's eslint-plugin-react-hooks installed (which Create React App and Vite include by default), it will warn you when you forget a dependency. Listen to those warnings — they're almost always right.

Quick check

You want to fetch data from an API exactly once when the component first appears on screen. Which useEffect call is correct?

Key takeaways

  • `useEffect` is where you handle side effects — anything that reaches outside React, like fetching data, timers, or subscriptions.
  • The dependency array controls when the effect runs: no array means every render, `[]` means once on mount, `[value]` means when that value changes.
  • Return a cleanup function from your effect to cancel timers, remove listeners, or undo anything that would otherwise leak.
  • Missing a dependency in the array causes stale data bugs; including a frequently-changing value without care can cause infinite loops.
  • For data fetching, the pattern is: empty array to fetch once, state variables for loading/data/error, and cleanup to handle fast prop changes.
Practice challenges
Test yourself · earn XP
0/4
Fix the bug#1

This code has a bug — what's wrong?

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

  useEffect(() => {
    setCount(count + 1);
  });

  return <p>{count}</p>;
}
Fill in the blank#2

This effect should re-run only when `userId` changes. Fill in the second argument to useEffect so it re-fetches whenever userId is different.

useEffect(() => {
  fetch(`https://api.example.com/users/${userId}`)
    .then(res => res.json())
    .then(data => setUser(data));
}, );
Reorder the lines#3

Arrange these lines into a correct useEffect that sets up a resize listener and cleans it up on unmount.

1
}, []);
2
  window.addEventListener('resize', handleResize);
3
  const handleResize = () => setWidth(window.innerWidth);
4
useEffect(() => {
5
  return () => window.removeEventListener('resize', handleResize);
Predict the output#4

What does the console log print across the first render and one later re-render caused by a state update elsewhere in the component?

predict-output
function Greeting() {
  const [name] = useState('Ada');

  useEffect(() => {
    console.log('effect ran');
  }, []);

  return <h2>Hi {name}</h2>;
}
Your turn
Practice exercise

Build a WindowWidth component that displays the current width of the browser window in pixels. It should update in real time as the user resizes the window. Use useEffect to add a resize event listener, and clean it up when the component unmounts.

Try it live — edit the code and hit Run to see it rendered:

solution.jsx · editable