Rendering JSX from props and state covers a lot of ground, but real apps also need to do things that are not really "rendering" at all: fetching data from a server, setting up a timer, subscribing to a WebSocket. React calls these side effects, and the useEffect hook is where they belong.

The basic shape

import { useEffect, useState } from "react";

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

  useEffect(() => {
    const id = setInterval(() => setTime(new Date()), 1000);
    return () => clearInterval(id); // cleanup when the component unmounts
  }, []);

  return <p>{time.toLocaleTimeString()}</p>;
}

useEffect takes a function to run, and a dependency array as the second argument. An empty array [] means "run this once, right after the first render, and never again." The function you return from inside the effect is the cleanup — React calls it before the component disappears, which is exactly where you cancel timers or subscriptions to avoid leaks.

Fetching data from a real API

This is the most common use of useEffect in real apps: load something from a backend as soon as the component mounts.

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/users")
      .then((res) => {
        if (!res.ok) throw new Error("Request failed");
        return res.json();
      })
      .then((data) => setUsers(data))
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <p>Loading users...</p>;
  if (error) return <p>Something went wrong: {error}</p>;

  return (
    <ul>
      {users.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}

Three pieces of state — the data itself, a loading flag, and an error message — is the standard shape for almost any fetch in React. Skipping the loading and error states is the single most common shortcut beginners take, and it is exactly what makes a real app feel broken the moment the network is slow or something fails.

The dependency array, properly

Whatever variables your effect reads from the component (props, state) should be listed in the dependency array, so the effect re-runs whenever they change:

useEffect(() => {
  fetch(`/api/users/${userId}`)
    .then((res) => res.json())
    .then(setUser);
}, [userId]); // re-fetch whenever userId changes

Leave the array off entirely and the effect runs after every single render, which is rarely what you want for a network call. Get comfortable reading and trusting the dependency array — it is one of the more confusing parts of React at first, but it becomes second nature fast.

You now have everything needed for a real app: components, props, state, lists and data fetching. The next two tutorials put all of it together into two actual projects.