ReactComponent Lifecycle

Component Lifecycle

Every React component passes through three phases during its lifetime: it mounts (appears in the DOM), it updates (re-renders when state or props change), and it unmounts (is removed from the DOM). Understanding these phases — and which hooks map to each — is foundational for writing correct side effects, subscriptions, and cleanup logic.

The Three Phases
  • Mount — the component is created and inserted into the DOM for the first time. This is when initial data fetching, subscriptions, and DOM measurements typically happen.

  • Update — the component re-renders because its state or props changed. React re-runs your function, computes a diff, and patches the DOM.

  • Unmount — the component is removed from the DOM. This is when you must clean up subscriptions, timers, and event listeners to prevent memory leaks.

Phase 1 — Mount

Pass an empty dependency array [] to useEffect to run code exactly once, after the component first appears in the DOM:

JSX
import { useState, useEffect } from 'react'

function UserProfile({ userId }) {
  const [user, setUser] = useState(null)

  // Runs ONCE after the component mounts — equivalent to componentDidMount
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => setUser(data))
  }, [])   // ← empty array = run on mount only

  if (!user) return <p>Loading…</p>
  return <h1>{user.name}</h1>
}
Note
The effect runs AFTER the browser has painted the screen, not during render. This means the user briefly sees the component without the fetched data — which is why an initial loading state is important.
Phase 2 — Update

To run an effect whenever specific values change, list them as dependencies. React compares each dependency to its previous value using Object.is and re-runs the effect only when at least one has changed:

JSX
function UserProfile({ userId }) {
  const [user, setUser] = useState(null)

  // Runs on mount AND whenever userId changes — equivalent to componentDidUpdate
  // (but also fires on initial mount, unlike componentDidUpdate alone)
  useEffect(() => {
    setUser(null)   // clear previous user while loading
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => setUser(data))
  }, [userId])   // ← re-run when userId changes

  if (!user) return <p>Loading user {userId}…</p>
  return <h1>{user.name}</h1>
}

// Runs after every single render (rarely what you want for side effects):
useEffect(() => {
  document.title = `Rendered at ${Date.now()}`
})   // ← no dependency array
Phase 3 — Unmount (Cleanup)

Return a function from useEffect to run cleanup code when the component unmounts — or before the effect re-runs (if dependencies changed). This is equivalent to componentWillUnmount:

JSX
function OnlineStatus({ userId }) {
  const [isOnline, setIsOnline] = useState(false)

  useEffect(() => {
    // Setup: subscribe to a WebSocket
    const socket = new WebSocket(`wss://api.example.com/presence/${userId}`)

    socket.onmessage = event => {
      setIsOnline(event.data === 'online')
    }

    // Cleanup: runs when the component unmounts OR when userId changes
    return () => {
      socket.close()   // prevent memory leak and stale event handlers
    }
  }, [userId])

  return <span>{isOnline ? '🟢 Online' : '⚫ Offline'}</span>
}

The cleanup function is called in two situations:

  • Before the effect re-runs — if userId changes, React calls the cleanup to close the old socket before opening a new one for the new userId.

  • When the component unmounts — React calls cleanup one final time when the component is removed from the tree.

All Three Phases in One Component

JSX
import { useState, useEffect } from 'react'

function DocumentTitle({ title }) {
  // --- MOUNT + UPDATE ---
  useEffect(() => {
    const previousTitle = document.title
    document.title = title   // side effect: update the browser tab title

    // --- UNMOUNT (cleanup) ---
    return () => {
      document.title = previousTitle   // restore when component leaves
    }
  }, [title])   // re-run (and clean up first) whenever title changes

  return <h1>{title}</h1>
}

// Timeline when title changes from 'Home' to 'About':
// 1. React re-renders DocumentTitle with title='About'
// 2. React runs the cleanup from the previous effect → document.title = 'Home' (restore)
// 3. React runs the new effect → document.title = 'About'
Class Lifecycle Methods vs Hooks

Before hooks (React 16.8), class components used named lifecycle methods. Here is the complete mapping to their hook equivalents:

Class Method

Hook Equivalent

Notes

componentDidMount

useEffect(() => {}, [])

Runs once after first render

componentDidUpdate

useEffect(() => {}, [dep])

Runs after render when dep changes

componentWillUnmount

Return function from useEffect

Runs on unmount (and before re-run)

getDerivedStateFromProps

Compute during render

Derive state inline, no hook needed

shouldComponentUpdate

React.memo / useMemo

Bail out of re-render if props unchanged

getSnapshotBeforeUpdate

useLayoutEffect + ref

Rare; capture DOM value before paint

componentDidCatch

Error Boundary class component

No hook equivalent yet in stable React

getDerivedStateFromProps — Compute, Don't Store

In class components, getDerivedStateFromProps was used to update state in response to prop changes. With hooks, the right approach is simpler: compute the derived value during render instead of syncing it into state:

JSX
// ✗ Old class pattern: sync props into state
class FullName extends React.Component {
  static getDerivedStateFromProps(props) {
    return { fullName: `${props.firstName} ${props.lastName}` }
  }
  render() {
    return <p>{this.state.fullName}</p>
  }
}

// ✓ Hooks pattern: compute during render — no state, no effect needed
function FullName({ firstName, lastName }) {
  const fullName = `${firstName} ${lastName}`   // derived inline
  return <p>{fullName}</p>
}
shouldComponentUpdate → React.memo

JSX
// ✗ Class: manual shallow comparison
class ExpensiveList extends React.PureComponent {
  render() {
    return this.props.items.map(item => <Item key={item.id} {...item} />)
  }
}

// ✓ Hooks: React.memo for the same bail-out behaviour
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
  return items.map(item => <Item key={item.id} {...item} />)
})

// Custom comparison (like shouldComponentUpdate returning false):
const ExpensiveList = React.memo(
  function ExpensiveList({ items, onSelect }) {
    return items.map(item => (
      <Item key={item.id} item={item} onSelect={onSelect} />
    ))
  },
  (prevProps, nextProps) => prevProps.items === nextProps.items
  // return true = skip re-render (same output), false = re-render
)
useLayoutEffect — The Synchronous Variant

useLayoutEffect has the same signature as useEffect but fires synchronously after DOM mutations and before the browser paints. Use it when you need to measure the DOM or make synchronous DOM changes to prevent visual flicker. This is the hook-based equivalent of getSnapshotBeforeUpdate + componentDidUpdate:

JSX
import { useLayoutEffect, useRef, useState } from 'react'

function Tooltip({ text, targetRef }) {
  const tooltipRef = useRef(null)
  const [position, setPosition] = useState({ top: 0, left: 0 })

  // useLayoutEffect fires before paint — prevents flicker
  useLayoutEffect(() => {
    const target  = targetRef.current.getBoundingClientRect()
    const tooltip = tooltipRef.current.getBoundingClientRect()

    setPosition({
      top:  target.bottom + window.scrollY,
      left: target.left + (target.width - tooltip.width) / 2,
    })
  }, [targetRef])

  return (
    <div
      ref={tooltipRef}
      style={{ position: 'absolute', top: position.top, left: position.left }}
    >
      {text}
    </div>
  )
}
Warning
Prefer useEffect over useLayoutEffect unless you have a specific reason to block painting. useLayoutEffect runs synchronously and can delay when the user sees content. Use it only for DOM measurements and synchronous visual corrections.
Concurrent Mode and Effects

React 18's concurrent rendering changes when effects run. In concurrent mode, React can start rendering a component, pause, and resume later. However, effects always run after the entire commit phase — after React has flushed all DOM mutations. This means:

  • Effects are still guaranteed to fire after the component is visible in the DOM.

  • React 18 Strict Mode deliberately runs effects twice in development (mount → cleanup → mount) to surface components that are not cleanup-safe. This does not happen in production.

  • The cleanup function must fully reverse the setup — any subscription opened in setup must be closed in cleanup.

JSX
// React 18 Strict Mode dev behaviour — runs the effect twice:
useEffect(() => {
  console.log('setup')       // printed twice in dev
  const id = setInterval(() => tick(), 1000)
  return () => {
    console.log('cleanup')   // printed once between the two setups
    clearInterval(id)
  }
}, [])
// Production: setup runs once, cleanup runs on unmount.
// Dev Strict Mode: setup → cleanup → setup (to verify cleanup is correct).
Tip
If you see your effect firing twice during development and it causes a bug, that is a signal that your cleanup function is incomplete. Fix the cleanup rather than disabling Strict Mode — Strict Mode is surfacing a real problem that will eventually appear in production.