Lesson 4 of 12

useEffect & Side Effects

You will learn how to use `useEffect` for work outside rendering, such as fetching data, timers and subscriptions, and how to clean it up. Getting this right avoids leaks and duplicate .

JSX
import { useState, useEffect } from 'react'

function UserProfile({ userId }) {
  const [user, setUser]     = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError]   = useState(null)

  useEffect(() => {
    // The dependency array [userId] means:
    // "run this effect when userId changes"
    let cancelled = false   // cleanup flag

    setLoading(true)
    setError(null)

    fetch(`/api/users/${userId}`)
      .then(res => {
        if (!res.ok) throw new Error("Not found")
        return res.json()
      })
      .then(data => {
        if (!cancelled) setUser(data)
      })
      .catch(err => {
        if (!cancelled) setError(err)
      })
      .finally(() => {
        if (!cancelled) setLoading(false)
      })

    // Cleanup function — runs before next effect or on unmount
    return () => { cancelled = true }
  }, [userId])   // dependency array

  if (loading) return <p>Loading...</p>
  if (error)   return <p>Error: {error.message}</p>
  return <h1>{user.name}</h1>
}
JSX
import { useState, useEffect, useRef } from 'react'

// useEffect dependency array patterns:
// []         — run once after mount
// [a, b]     — run when a or b changes
// (omitted)  — run after every render (usually wrong)

// Timer example — with proper cleanup
function Stopwatch() {
  const [seconds, setSeconds] = useState(0)
  const [running, setRunning] = useState(false)

  useEffect(() => {
    if (!running) return
    const id = setInterval(() => setSeconds(s => s + 1), 1000)
    return () => clearInterval(id)   // cleanup clears the interval
  }, [running])

  return (
    <div>
      <p>{seconds}s</p>
      <button onClick={() => setRunning(r => !r)}>
        {running ? "Pause" : "Start"}
      </button>
      <button onClick={() => { setRunning(false); setSeconds(0) }}>Reset</button>
    </div>
  )
}

// useRef — mutable ref that doesn't trigger re-renders
function AutoFocusInput() {
  const inputRef = useRef(null)

  useEffect(() => {
    inputRef.current?.focus()   // focus on mount
  }, [])

  return <input ref={inputRef} placeholder="I'm focused!" />
}

Common question

Does every piece of logic belong in useEffect?

An effect keeps a component in sync with something outside React. Its the values that, when they change, mean it needs to sync again; its returned cleanup undoes the previous sync. That is the whole model, and it explains how every effect you write will behave.

Before writing one, ask what outside thing you are syncing with. If the answer is "nothing, I'm working out a value", compute it during render. If it is "responding to a click", put it in the event handler. Save effects for subscriptions, timers, manual work and data fetching, and always clean them up.

Source: The React documentation has a whole page called "You Might Not Need an Effect", listing common cases where an effect is the wrong tool.

Common mix-ups

Each of these sounds reasonable. Think about why it is wrong, then open the explanation.

  • An effect runs after render, so it's the natural place for anything that should happen when data changes.

  • An empty dependency array means the effect never runs.

  • Cleanup functions are optional tidiness.

Finished reading?Mark it done to track your progress.

Tip: use the ← and → keys to move between lessons.