ReReact · Lesson 4 of 8

useEffect & Side Effects

`useEffect` is where you handle things that happen outside the React render cycle: API calls, timers, subscriptions, DOM manipulation. It runs after every render, unless you tell it otherwise.

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!" />
}
✦ Tip
Always return a cleanup function from useEffect if you start something (timer, subscription, event listener, fetch). Without cleanup, you get memory leaks and "Can't perform state update on unmounted component" warnings. The `cancelled` flag pattern is especially important for fetch calls.