CodeMIRAI
0 XP

Side effects with useEffect

3 min read 5 runnable examples Quiz included

Rendering should only describe what's on screen. Anything else, such as starting a timer, fetching data, listening to the keyboard or changing the page title, is a side effect, and belongs in useEffect.

React (JSX)
import { useEffect, useState } from "react";

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

  useEffect(() => {
    document.title = `Clicked ${count} times`;
    console.log("effect ran, count is", count);
  });

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

The effect runs after React has updated the screen. With no second argument, it runs after every render: watch the console as you click.

The dependency array

React (JSX)
useEffect(() => { ... });            // after every render
useEffect(() => { ... }, []);        // once, after the first render
useEffect(() => { ... }, [userId]);  // after the first render, and whenever userId changes
time →renderrenderrenderunmountuseEffect(fn)after every renderuseEffect(fn, [])once, after the firstuseEffect(fn, [id])when id has changeda cleanup function returned from the effect runs before the next run, and on unmount
Effects run after React updates the screen. The dependency array decides how often; the cleanup undoes what the last run set up.
React (JSX)
import { useEffect, useState } from "react";

function App() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);

  useEffect(() => { console.log("runs once, after the first render"); }, []);
  useEffect(() => { console.log("a changed to", a); }, [a]);

  return (
    <div>
      <button onClick={() => setA(a + 1)}>a = {a}</button>{" "}
      <button onClick={() => setB(b + 1)}>b = {b}</button>
      <p>Click both and watch the console: changing b never runs the "a changed" effect.</p>
    </div>
  );
}

List every value from the component that the effect uses. React then re-runs the effect exactly when one of them changes.

Cleaning up

React (JSX)
import { useEffect, useState } from "react";

function Clock() {
  const [now, setNow] = useState(new Date());

  useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(id);       // the cleanup: stop the timer
  }, []);

  return <h2>{now.toLocaleTimeString()}</h2>;
}

function App() {
  const [show, setShow] = useState(true);
  return (
    <div>
      <button onClick={() => setShow(!show)}>{show ? "Hide" : "Show"} the clock</button>
      {show && <Clock />}
    </div>
  );
}

The function an effect returns is its cleanup. React calls it when the component disappears, and before running the effect again. Without it, a hidden clock's timer would keep ticking in the background for as long as the page stays open.

Listening to the keyboard

React (JSX)
import { useEffect, useState } from "react";

function App() {
  const [key, setKey] = useState("(none yet)");

  useEffect(() => {
    const onKey = (e) => setKey(e.key === " " ? "Space" : e.key);
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  return <p>Click inside this preview, then press any key. Last key: <strong>{key}</strong></p>;
}

Listeners on window or document live outside React, so the effect adds the listener and its cleanup removes it.

Fetching data

React (JSX)
import { useEffect, useState } from "react";

// a pretend server: answers after 800 ms, and fails for an unknown id
function fetchCourse(id) {
  const courses = { 1: { title: "Python", lessons: 64 }, 2: { title: "SQL", lessons: 23 }, 3: { title: "React", lessons: 12 } };
  return new Promise((resolve, reject) =>
    setTimeout(() => (courses[id] ? resolve(courses[id]) : reject(new Error("Course not found"))), 800)
  );
}

function App() {
  const [id, setId] = useState(1);
  const [course, setCourse] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let ignore = false;               // set when id changes before the answer arrives
    setLoading(true);
    setError(null);
    fetchCourse(id)
      .then((data) => { if (!ignore) setCourse(data); })
      .catch((err) => { if (!ignore) setError(err.message); })
      .finally(() => { if (!ignore) setLoading(false); });
    return () => { ignore = true; };
  }, [id]);

  return (
    <div>
      {[1, 2, 3, 4].map((n) => (
        <button key={n} onClick={() => setId(n)} style={{ marginRight: 6, fontWeight: n === id ? 700 : 400 }}>Course {n}</button>
      ))}
      {loading ? <p>Loading...</p>
        : error ? <p style={{ color: "crimson" }}>{error}</p>
        : <p>{course.title}: {course.lessons} lessons</p>}
    </div>
  );
}

A real app calls fetch(url) in the same place. The pretend server just keeps the example working without a network:

React (JSX)
useEffect(() => {
  let ignore = false;
  fetch(`https://api.example.com/courses/${id}`)
    .then((r) => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
    .then((data) => { if (!ignore) setCourse(data); })
    .catch((err) => { if (!ignore) setError(err.message); });
  return () => { ignore = true; };
}, [id]);

The ignore flag handles a race. Click quickly from course 1 to course 2, and course 1's slower answer could arrive last and overwrite course 2's. The cleanup marks the old request as one to ignore.

Finished this lesson?Mark it complete to earn +20 XP and move your progress on.