DevNotes

DevNotes

@devnotes ·

React Part 7: Load data with useEffect

So far, components have only drawn things. Sometimes a component also needs to reach outside React: load data from a server, start a timer, or change the tab's title. That's what useEffect is for.

The basic idea

JSX
import { useEffect, useState } from "react";
function Clock() {  const [time, setTime] = useState(new Date());
  useEffect(() => {    const id = setInterval(() => setTime(new Date()), 1000);    return () => clearInterval(id);  }, []);
  return <p>{time.toLocaleTimeString()}</p>;}

useEffect takes a function, and React runs it after the component appears on the screen. The function you return from it is the cleanup. React runs it before running the effect again, and when the component goes away, so the timer doesn't keep ticking for a clock nobody can see.

The dependency array

The second argument tells React when to run the effect again:

  • []: once, after the component first appears.
  • [userId]: after it first appears, and again whenever userId changes.
  • No array at all: after every single draw. You'll rarely want that.

List every prop and state value the effect uses. Leave one out and the effect keeps using an old value.

Loading data

JSX
function UserCard({ userId }) {  const [user, setUser] = useState(null);  const [error, setError] = useState("");
  useEffect(() => {    let ignore = false;
    async function load() {      try {        const res = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);        if (!res.ok) throw new Error(`Status ${res.status}`);        const data = await res.json();        if (!ignore) {          setUser(data);          setError("");        }      } catch {        if (!ignore) setError("Couldn't load this user.");      }    }
    load();    return () => {      ignore = true;    };  }, [userId]);
  if (error) return <p>{error}</p>;  if (!user) return <p>Loading...</p>;  return <h2>{user.name}, {user.address.city}</h2>;}

How the pieces fit:

  • Two pieces of state cover the three things a person can see: loading, an error, or the user.
  • The effect depends on userId, so it loads again whenever that prop changes.
  • After the first load, a new userId keeps the old user on screen until the new one arrives, so "Loading..." shows only the first time.
  • The ignore flag handles a race. If userId changes while the first request is still on its way, the cleanup marks that request as stale, so a slow, old answer can't overwrite the newer one.

The function you give useEffect can't be async itself, because React expects it to return nothing or a cleanup function. So the effect defines load inside and calls it. (A catch without a name in brackets is fine when you don't need the error itself.)

It runs twice while you're developing

In development, when a component first appears, React runs each effect, cleans it up, and runs it again. That's deliberate. The Vite starter wraps your app in <StrictMode>, which does this to catch effects that forget to clean up. It doesn't happen on your live site. If the double run breaks something, your cleanup is missing.

When you don't need an effect

Effects are for talking to things outside React. Don't use them to work out values from props or state, like filtering a list or joining a first and last name. Calculate those directly while drawing, as in React Part 6. An effect there only adds an extra draw and a place for bugs to hide.

In bigger apps, most teams load data with a library like TanStack Query, or a framework like Next.js, which handle caching and loading states for you. Knowing how useEffect works makes both of those much easier to follow.

Try it

Make a page with a number input for a user id, from 1 to 10, and a UserCard below it. Pass the number in with Number(e.target.value), and show the user's name, email and city with a loading and an error message.


Next: React Part 8: Build a to-do app

reactjavascriptwebdevbeginners

Join WIK to like and reply to DevNotes and see more like this.