Start.dev
Back to blog

React useEffect explained, with simple examples

What useEffect really does, when it runs, and how the dependency array and cleanup function work. Simple examples for fetching data, timers and page titles.

Ariel Weinberger
Ariel Weinberger ยท 5 min read

useEffect is the hook that confuses almost everyone at first. It looks like a small function with an odd array at the end, and it seems to run at random moments. It isn't random. Once you know the one idea behind it, the rest falls into place.

Short answer: useEffect lets a component do something after it has rendered, such as fetching data, starting a timer, or updating the page title. You tell React what to do and when to do it again, and React handles the timing.

If you're new to hooks, it helps to be comfortable with state first. Start with State With Usestate (opens in a new tab), then come back here.

What is an effect?

A React component has one main job: take props and state, and return JSX. That work should be predictable, with no surprises. React can call your component many times, so rendering must not do anything that affects the outside world.

But real apps need to touch the outside world. They load data from a server, set up subscriptions, and change things React doesn't control, like document.title. Those jobs are called side effects, and useEffect is where they live.

The lesson Running Code After Render (opens in a new tab) walks through this idea step by step.

Your first effect

Here is a component that updates the browser tab title whenever the count changes.

import { useState, useEffect } from "react";
 
function Counter() {
  const [count, setCount] = useState(0);
 
  useEffect(() => {
    document.title = `Clicked ${count} times`;
  }, [count]);
 
  return (
    <button onClick={() => setCount((c) => c + 1)}>
      Clicked {count} times
    </button>
  );
}

There are two arguments. The first is a function: the effect itself. The second is the dependency array: a list of values the effect depends on. React renders the component, updates the screen, and only then runs your effect.

The dependency array decides when it runs

This is the part that trips people up. The array controls how often the effect runs.

Dependency arrayWhen the effect runs
Left outAfter every render
[]Once, after the first render
[count]After the first render, and again whenever count changes
// Runs after every render (rarely what you want)
useEffect(() => {
  console.log("rendered");
});
 
// Runs once, when the component first appears
useEffect(() => {
  console.log("mounted");
}, []);
 
// Runs when userId changes
useEffect(() => {
  console.log("user changed", userId);
}, [userId]);

A good rule: list every value from your component that the effect uses. If the effect reads userId, then userId goes in the array. The lesson The Dependency Array (opens in a new tab) covers this in detail.

Two JavaScript ideas explain most dependency bugs. First, an effect captures the values from the render it belongs to, which is a closure. If that sounds fuzzy, read JavaScript closures explained with everyday code. Second, React compares dependencies by identity. A new object or array created on each render counts as "changed" every time, so the effect keeps re-running. Value vs reference in JavaScript explains why.

Example: fetching data

The most common use of useEffect is loading data when a component appears.

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    async function loadUsers() {
      try {
        const response = await fetch("https://jsonplaceholder.typicode.com/users");
        if (!response.ok) throw new Error("Request failed");
        setUsers(await response.json());
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }
 
    loadUsers();
  }, []);
 
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Something went wrong: {error}</p>;
 
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

A few things to notice:

The fetch itself is plain JavaScript. If you want to practise that side, Your First Fetch (opens in a new tab) and Handling Fetch Errors (opens in a new tab) cover it before React enters the picture.

Example: refetching when something changes

Put a value in the dependency array and the effect re-runs when it changes. Here we load a different user each time userId changes.

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
 
  useEffect(() => {
    async function loadUser() {
      const response = await fetch(`/api/users/${userId}`);
      setUser(await response.json());
    }
 
    loadUser();
  }, [userId]);
 
  return user ? <h2>{user.name}</h2> : <p>Loading...</p>;
}

This pattern powers search pages, filters, and anything that depends on a route parameter. The lesson Refetching When Data Changes (opens in a new tab) builds exactly this.

Cleanup: tidying up after yourself

Some effects start something that keeps going, like a timer or an event listener. If you never stop it, it keeps running after the component is gone. To prevent that, return a cleanup function from the effect.

function Clock() {
  const [seconds, setSeconds] = useState(0);
 
  useEffect(() => {
    const id = setInterval(() => {
      setSeconds((s) => s + 1);
    }, 1000);
 
    return () => clearInterval(id);
  }, []);
 
  return <p>{seconds} seconds</p>;
}

React runs the cleanup in two situations: before the effect runs again, and when the component is removed from the screen. That's why the interval above is cleared properly. Cleanup is also how you ignore a slow response from an outdated request. The lesson Cleanup Functions (opens in a new tab) shows both cases.

In development, React's Strict Mode runs effects twice on purpose, mounting, cleaning up, and mounting again. If your effect misbehaves when this happens, it usually means a cleanup is missing. It's a feature, not a bug.

Common mistakes

  • Forgetting the dependency array. The effect runs after every render. If it also sets state, you get an infinite loop.
  • Leaving out a dependency. The effect uses an old value and shows stale data.
  • Putting an object or function in the array without thinking. It's a new one every render, so the effect re-runs every time.
  • Using an effect when you don't need one. If you can work something out from props or state while rendering, do it there. Filtering a list or adding up a total doesn't need an effect.
  • Skipping cleanup for timers, listeners and subscriptions.

That fourth point is worth remembering. Effects are for syncing with things outside React. If you're only transforming data you already have, use plain JavaScript, such as array methods like map, filter and reduce. And if your state updates are growing tangled, useState vs useReducer can help you decide how to organise them.

Reusing effects with custom hooks

Once you've written the same fetch effect three times, you'll want to share it. A custom hook lets you wrap the state and the effect together, so components just call useFetch(url). See Building A Custom Hook (opens in a new tab) and A Usefetch Hook (opens in a new tab).

Quick recap

  • useEffect runs your code after React renders.
  • The dependency array controls when it runs: every render, once, or when listed values change.
  • List every value the effect uses.
  • Return a cleanup function for timers, listeners and subscriptions.
  • Don't use an effect for things you can calculate during render.

The best way to make this stick is to build with it. Work through React Fundamentals (opens in a new tab) to practise effects inside real components, and brush up on the JavaScript underneath with Intermediate JavaScript (opens in a new tab).

Level up your coding skills

Jump into interactive courses and start building real-world projects.

Start learning

Related posts

useState or useReducer? See the same cart built both ways, the signs it's time to switch, and a simple rule of thumb for choosing in your React components.
Ariel Weinberger
Ariel Weinberger ยท 5 min read
A plain-English guide to JavaScript async/await: what it really does, how it relates to promises, and the mistakes that trip people up.
Ariel Weinberger
Ariel Weinberger ยท 5 min read
Learn JavaScript closures with tested examples: counters, the var vs let loop question, private state, debounce, and the stale closure bug in React hooks.
Brad Traversy
Brad Traversy ยท 8 min read