ReactuseEffect: Side Effects

useEffect: Side Effects

React components are supposed to be pure — given the same props and state, they always return the same JSX, with no observable side effects during rendering. But real applications need to interact with the world outside React: fetch data from a server, subscribe to events, update the browser tab title, or start a timer. These operations are called side effects, and useEffect is how you run them safely inside function components.

What Is a Side Effect?

A side effect is anything that affects something outside the scope of the current function call — anything that React does not control directly. Common examples include:

  • Data fetching — calling fetch() or a GraphQL query

  • Subscriptions — adding event listeners to window, a WebSocket, or an observable

  • DOM manipulation — focusing an input, measuring an element, or calling a third-party DOM library

  • TimerssetTimeout and setInterval

  • Logging — sending analytics events

  • Syncing with external systems — updating localStorage, syncing with a server, writing to a ref

The mental model: effects synchronize React state to external systems. When state changes, React re-renders, and your effect keeps whatever external system in sync with the new state.

The useEffect Signature

JSX
useEffect(
  setup,    // function to run after render
  deps?     // optional dependency array
)
  • setup — a function containing your side-effect logic. It may optionally return a cleanup function.

  • deps — an optional array of reactive values the effect depends on. React uses this to decide when to re-run the effect.

When Effects Run

Effects do not run during rendering. They run after React has committed changes to the DOM — the browser has already painted the new UI. This ordering is intentional: if your effect mutates the DOM (e.g., measuring an element), React needs the DOM to be up to date first.

Phase

What happens

Render

React calls your component function and computes the new JSX

Commit

React updates the DOM to match the new JSX

Paint

Browser paints pixels on screen

Effect

useEffect callback runs — after paint

Example 1: Syncing document.title with State

The simplest effect: keep the browser tab title in sync with a counter. Without useEffect you would have to remember to update the title every time the counter changes. With useEffect, React handles the synchronization for you.

JSX
import { useState, useEffect } from 'react'

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

  // This effect runs after every render where 'count' changed.
  useEffect(() => {
    document.title = `You clicked ${count} times`
  }, [count])

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  )
}

On every render where count has changed, React updates document.title to reflect the new value. The [count] dependency array tells React: "only re-run this effect when count changes."

Example 2: Subscribing to window resize

Event listeners are a classic side effect. You attach a listener when the component mounts and must remove it when the component unmounts — otherwise you create a memory leak (the listener keeps a reference to your component even after it is gone).

JSX
import { useState, useEffect } from 'react'

function WindowWidth() {
  const [width, setWidth] = useState(window.innerWidth)

  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth)
    }

    window.addEventListener('resize', handleResize)

    // Cleanup: remove the listener when the component unmounts
    return () => {
      window.removeEventListener('resize', handleResize)
    }
  }, []) // [] = run once on mount, clean up on unmount

  return <p>Window width: {width}px</p>
}

The function returned from setup is the cleanup function. React calls it before running the effect again and when the component unmounts. More on cleanup in the next lesson.

Example 3: Fetching Data on Mount

Fetching data when a component first renders is one of the most common uses of useEffect. The key is to handle the case where the component unmounts before the fetch completes — otherwise you will try to update state on an unmounted component.

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(() => {
    // AbortController lets us cancel the fetch if the component unmounts
    const controller = new AbortController()

    setLoading(true)
    setError(null)

    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then(res => {
        if (!res.ok) throw new Error('Failed to fetch user')
        return res.json()
      })
      .then(data => {
        setUser(data)
        setLoading(false)
      })
      .catch(err => {
        // Ignore errors caused by our own abort
        if (err.name !== 'AbortError') {
          setError(err.message)
          setLoading(false)
        }
      })

    // Cleanup: abort the in-flight request if userId changes or component unmounts
    return () => controller.abort()
  }, [userId]) // Re-run when userId changes

  if (loading) return <p>Loading...</p>
  if (error) return <p>Error: {error}</p>
  return <h1>{user.name}</h1>
}
Note
This pattern works but has limitations (no caching, no deduplication). For production data fetching, consider React Query, SWR, or a framework-level data loader. The React team recommends using a library rather than raw `useEffect` for data fetching in complex apps.
The Cleanup Function

Every effect can optionally return a cleanup function. React calls the cleanup:

  • Before running the effect again (when deps changed and the effect is about to re-run)

  • When the component unmounts

JSX
useEffect(() => {
  // setup — runs after render
  const subscription = subscribeToData(id, handleData)

  return () => {
    // cleanup — runs before next effect OR on unmount
    subscription.unsubscribe()
  }
}, [id])
The Mental Model

Think of useEffect not as "run code after render" but as "synchronize an external system with React state":

  • document.title should equal count → effect keeps it in sync

  • The resize listener should exist while this component is on screen → effect attaches on mount, cleans up on unmount

  • The user data should match userId → effect fetches whenever userId changes

Tip
When you find yourself writing an effect, ask: "What external system am I synchronizing with?" If the answer is "nothing" — if the code is just transforming data or computing a value — you probably don't need an effect at all.
Rules of Hooks (apply to useEffect too)
  • Call useEffect at the top level of your component, not inside loops, conditions, or nested functions

  • Call useEffect only in function components or custom hooks

Warning
Never call `useEffect` inside an `if` statement. The order of hook calls must be the same on every render. Use a condition *inside* the effect instead: `useEffect(() => { if (enabled) { ... } }, [enabled])`