React Basics

Session 3 — useEffect, Fetching Data & Error Handling

45 minutes
📚 Beginner
🎯 Side Effects
Scroll to begin
Part 2 · Lifecycle

Component Lifecycle

Every React component goes through three phases during its life. Understanding these phases is the key to knowing when your code runs and why certain bugs happen.

🌱

Mount

Component appears on screen for the first time

  • Component function runs
  • JSX is rendered to the DOM
  • Effects with [] run once
🔄

Update

State or props change — component re-renders

  • Component function re-runs
  • DOM is patched (not replaced)
  • Effects re-run if deps changed
💀

Unmount

Component is removed from the screen

  • DOM nodes are removed
  • State is discarded
  • Effect cleanup functions run
Part 3 · Side Effects

What is useEffect?

A side effect is anything that reaches outside the component — fetching data, setting a timer, updating the page title, or subscribing to events. React's render function should be pure, so effects live separately in useEffect.

useEffect — Basic Syntax
import { useState, useEffect } from 'react'

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

  // useEffect(callback, dependencyArray)
  useEffect(() => {
    // This runs AFTER the component renders
    document.title = `You clicked ${count} times`
  }, [count]) // ← re-run whenever count changes

  return (
    <button onClick={() => setCount(count + 1)}>
      Click me
    </button>
  )
}
📡

Data Fetching

Load data from an API when the component mounts or when a search term changes.

⏱️

Timers

Set up setInterval or setTimeout — and clean them up when the component unmounts.

📄

DOM Side Effects

Update the page title, focus an input, or integrate with non-React libraries.

🔌

Subscriptions

Subscribe to WebSockets, browser events, or external stores.

Part 4 · The Dependency Array

Controlling When Effects Run

The second argument to useEffect — the dependency array — controls when the effect re-runs.

Three Modes of useEffect
// 1. No dependency array — runs after EVERY render
useEffect(() => {
  console.log('Runs after every render')
})

// 2. Empty array [] — runs ONCE after first render (mount)
useEffect(() => {
  console.log('Runs once on mount')
  // Great for initial data fetching
}, [])

// 3. Array with values — runs when those values change
useEffect(() => {
  console.log('userId changed to:', userId)
}, [userId]) // only re-runs when userId changes
Dependency Array When Effect Runs Use Case
Omitted After every render Sync with every state change (rare)
[ ] empty Once on mount Fetch data on page load
[a, b] When a or b changes Re-fetch when search query changes
⚠️ The Rules of Dependencies

Include every variable from the component that the effect uses. Leaving something out can cause stale closure bugs — your effect reads an old value instead of the current one.

Part 5 · APIs

Fetching Data from an API

The standard React pattern for data fetching uses an async function inside the effect callback — you can't make the callback itself async.

UserProfile.jsx — Standard Fetch Pattern
import { useState, useEffect } from 'react'

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

  useEffect(() => {
    // Define async function inside effect
    async function fetchUser() {
      try {
        setLoading(true)
        const res = await fetch(`/api/users/${userId}`)
        if (!res.ok) throw new Error('User not found')
        const data = await res.json()
        setUser(data)
      } catch (err) {
        setError(err.message)
      } finally {
        setLoading(false)
      }
    }

    fetchUser() // call it immediately
  }, [userId]) // re-fetch when userId changes

  if (loading) return <p>Loading...</p>
  if (error)   return <p>Error: {error}</p>

  return <h2>Hello, {user.name}!</h2>
}
💡 Why Not async Directly?

An async function returns a Promise. React expects useEffect's callback to return either nothing or a cleanup function — not a Promise. So always define the async function inside and call it.

Same Thing with Axios

Axios is a popular HTTP library that simplifies fetching — it parses JSON automatically, throws on non-2xx responses, and has built-in cancellation support.

UserProfile.jsx — Axios Version
import { useState, useEffect } from 'react'
import axios from 'axios'

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

  useEffect(() => {
    // Axios uses CancelToken for cleanup
    const controller = new AbortController()

    async function fetchUser() {
      try {
        setLoading(true)
        // axios.get returns { data, status, headers, ... }
        // No need to call .json() — axios does it automatically
        // No need to check response.ok — axios throws on 4xx/5xx
        const { data } = await axios.get(`/api/users/${userId}`, {
          signal: controller.signal   // ties request to AbortController
        })
        setUser(data)
      } catch (err) {
        if (axios.isCancel(err)) return // component unmounted — ignore
        setError(err.response?.data?.message ?? err.message)
      } finally {
        setLoading(false)
      }
    }

    fetchUser()
    return () => controller.abort() // cancel if userId changes or unmounts
  }, [userId])

  if (loading) return <p>Loading...</p>
  if (error)   return <p>Error: {error}</p>

  return <h2>Hello, {user.name}!</h2>
}
fetch (built-in) axios (library)
JSON parsing Manual — res.json() Automatic — result is in data
Error on 4xx/5xx No — must check res.ok Yes — throws automatically
Request cancellation AbortController AbortController or CancelToken
Install needed? No — built into browsers Yes — npm install axios
Part 6 · UX Patterns

Loading States & Error Handling

A great user experience shows what's happening. Always handle three states: loading, error, and success.

1

Start with loading = true

Assume data isn't ready yet. Show a spinner or skeleton screen while waiting.

2

Catch errors with try/catch

Network can fail. The API can return errors. Always wrap your fetch in try/catch.

3

Set loading = false in finally

Use finally so loading is always turned off — whether the fetch succeeded or failed.

4

Use early returns to render each state

Return different JSX for loading, error, and the happy path.

🔴 Live Demo — Simulated API Fetch

Click fetch to simulate a 1.5s API call:

Press the button to load data.
🧠 Check the Response Status

fetch() does not throw on HTTP error codes (404, 500). You must manually check response.ok or response.status and throw an error yourself if needed.

Part 7 · Memory Leaks

Cleaning Up Effects

If your effect starts something ongoing — like a timer or event listener — you must stop it when the component unmounts. Return a cleanup function from your effect.

What Is a Memory Leak?

A memory leak happens when your code holds onto resources (memory, connections, timers) that it no longer needs and never releases. Over time those resources pile up — the browser gets slower, crashes, or starts behaving in unexpected ways.

In React, the most common cause is an effect that keeps running after the component it belongs to has been removed from the screen.

🔍 The Exact Problem — Step by Step
  1. User navigates to a page — <UserProfile> mounts and starts fetching data.
  2. Before the request finishes, the user clicks "Back" — React unmounts the component.
  3. The fetch completes anyway and calls setUser(data) — but the component is gone.
  4. React tries to update state on a dead component. It warns you and wastes memory keeping that state alive for nothing.
  5. If this happens on every navigation, leaks accumulate and the app slows down.
The Leak — No Cleanup
useEffect(() => {
  async function fetchUser() {
    const res = await fetch(`/api/users/${userId}`)
    const data = await res.json()
    setUser(data) // ⚠️ might run after the component unmounted!
  }
  fetchUser()
  // No return → no cleanup → fetch keeps going even after unmount
}, [userId])
The Fix — AbortController
useEffect(() => {
  const controller = new AbortController() // create a cancel handle

  async function fetchUser() {
    try {
      const res = await fetch(`/api/users/${userId}`, {
        signal: controller.signal   // link request to the handle
      })
      const data = await res.json()
      setUser(data) // only runs if the component is still mounted
    } catch (err) {
      if (err.name === 'AbortError') return // cancelled — not a real error
      setError(err.message)
    }
  }

  fetchUser()

  // React calls this when the component unmounts OR before re-running the effect
  return () => controller.abort() // ✅ request is cancelled, no stale setState
}, [userId])
⏱️

Timer Cleanup

A setInterval keeps firing after unmount, causing stale state updates on every tick.

return () => clearInterval(timer)
🔌

Event Listener Cleanup

Without removal, the same handler is registered on every render — it stacks up silently.

return () => window.removeEventListener('resize', fn)
📡

Fetch / Axios Cleanup

An in-flight request completes after unmount and tries to update state that no longer exists.

return () => controller.abort()
🔗

WebSocket / Subscription Cleanup

An open socket keeps receiving messages and calling setState even though the UI is gone.

return () => socket.close()
🧠 The Mental Model

Think of useEffect like renting a car. When you return the car (unmount), you hand back the keys — you don't keep driving it. The cleanup function is handing back the keys: cancel the fetch, clear the timer, remove the listener. If you forget, someone else pays for the damage.

Part 8 · Wrap-Up

Recap & Quick Quiz

Let's review the key concepts from today's session.

useEffect

Runs after render. Used for data fetching, timers, DOM updates, and subscriptions.

Dependency Array

Controls when the effect re-runs: omit for always, [] for once, [x] when x changes.

3 States Pattern

Always handle loading, error, and success in components that fetch data.

Cleanup

Return a cleanup function from useEffect to stop timers and subscriptions.

🧪 Quiz — Question 1 of 3

What does an empty dependency array [] tell useEffect?

🧪 Quiz — Question 2 of 3

Why can't you make the useEffect callback async directly?

🧪 Quiz — Question 3 of 3

Does fetch() throw an error when the server returns 404?
🎯 What's Next?

In Session 4, we dive deep into React Router — SPAs, route setup, Link & NavLink, URL parameters, useNavigate, and nested layouts.