useEffect

Course 1 · Ch 8
useEffect and Side Effects
Running code in response to a render, rather than in response to a specific event

Everything so far has either rendered JSX or responded to a user-triggered event like a click. A side effect is anything that reaches outside the rendering process itself — fetching data, setting a timer, manually changing the document title, subscribing to something external. useEffect is the hook for running that kind of code at the right moment, tied to a component's render rather than to a click.

The Basic Shape

import { useEffect, useState } from "react"; function PageTitle({ title }) { useEffect(() => { document.title = title; }, [title]); return null; }

useEffect takes two arguments: a function containing the side effect itself, and a dependency array listing which values it depends on. React re-runs the effect function whenever any value in that array has changed since the last render — here, whenever title changes, the document's actual browser tab title updates to match.

The Three Forms of the Dependency Array

useEffect(() => { /* ... */ }); // no array — runs after every single render useEffect(() => { /* ... */ }, []); // empty array — runs once, after the first render only useEffect(() => { /* ... */ }, [userId]); // runs after the first render, and again whenever userId changes

These three shapes cover almost every use case. No array at all is rare in practice — re-running on literally every render is usually a sign something belongs in the dependency array instead. An empty array is the standard choice for "run this once when the component first appears" (fetching initial data, setting up a subscription). A populated array is for "run this again whenever this specific thing changes."

Fetching Data on Mount

function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { fetch(`/api/users/${userId}`) .then((res) => res.json()) .then((data) => setUser(data)); }, [userId]); if (!user) return <p>Loading...</p>; return <h2>{user.name}</h2>; }

This is the classic shape of a data-fetching effect: state to hold the result (starting as null, meaning "not loaded yet"), an effect that fetches and calls the setter once the data arrives, and a conditional render that shows a loading state until user stops being null. [userId] ensures the fetch re-runs if a different user's profile needs to be shown — without it, switching to a new userId prop wouldn't trigger a new fetch at all. (Intermediate Chapter 6 covers more complete error/loading handling for this exact pattern.)

Cleaning Up After an Effect

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

Returning a function from inside the effect gives React a cleanup function, run right before the effect runs again, and also when the component is removed from the page entirely. setInterval here would otherwise keep ticking forever, even after Clock unmounts — clearInterval(id) in the cleanup function stops it at exactly the right moment. Anything that "starts" something ongoing (a timer, an event listener, a subscription) almost always needs a matching cleanup function to "stop" it.

Missing dependencies cause stale or infinite-loop bugs
Leaving a value out of the dependency array that the effect actually uses means the effect can run with an outdated value, since React doesn't know to re-run it when that value changes. The opposite mistake — including too much, such as a value the effect itself updates — can cause the effect to re-run every single render in an infinite loop. Most editors flag this automatically via an ESLint rule (exhaustive-deps); when in doubt, list every state/prop value the effect function actually reads.
Dependency arrayEffect runs...
(none)After every render — rarely what's actually wanted
[]Once, right after the first render
[value]After the first render, and again whenever value changes

Coding Challenges

Challenge 1

Build a component that uses useEffect with an empty dependency array to set document.title to "Welcome!" once, when the component first appears.

📄 View solution
Challenge 2

Build a Stopwatch component with a seconds count state that increases by 1 every second using setInterval inside useEffect, with a correct cleanup function that clears the interval.

📄 View solution
Challenge 3

Build a component that takes a query prop and uses useEffect with [query] as the dependency array to log "Searching for: [query]" to the console every time query changes (simulate changing it with a couple of useState-driven buttons in the parent).

📄 View solution

Chapter 8 Quick Reference

  • useEffect(fn, deps) — runs fn after a render, based on the dependency array
  • No array — every render; [] — once on mount; [value] — on mount and whenever value changes
  • Returning a function from the effect is a cleanup function — runs before the next effect, and on unmount
  • Anything that "starts" something ongoing (timer, listener, subscription) needs a matching cleanup
  • Missing dependencies → stale values; extra/wrong dependencies → possible infinite loops
  • Next chapter: component composition and children — building flexible wrapper components