Back to React
React
medium
mid

How does React handle the component lifecycle in functional components?

Function components don't have lifecycle methods — they re-run top-to-bottom on every render. Hooks emulate the lifecycle: useState/useRef = instance fields; useEffect with [] = componentDidMount + componentWillUnmount; useEffect with deps = componentDidUpdate; useLayoutEffect = synchronous post-commit work; the function body itself = render. Reasoning shifts from 'phases' to 'this state at this moment'.

6 min read·~5 min to think through

Class lifecycle methods don't exist for function components — hooks replace them.

Mental model shift

A function component is a render function. Every render produces a snapshot of UI for a given state. Hooks bridge that snapshot to React's machinery.

Mapping

Class methodFunction equivalent
constructoruseState initializer; useRef for instance vars
componentDidMountuseEffect(() => {...}, [])
componentDidUpdateuseEffect(() => {...}, [deps])
componentWillUnmountcleanup returned from useEffect
shouldComponentUpdateReact.memo + custom compare
getDerivedStateFromPropscompute in render, or use a key to reset
getSnapshotBeforeUpdateuseLayoutEffect
componentDidCatcherror boundaries (still class-based, but consumable from FCs)

Side-by-side

tsx
// Class
class Clock extends Component {
  state = { time: new Date() };
  componentDidMount() { this.id = setInterval(this.tick, 1000); }
  componentWillUnmount() { clearInterval(this.id); }
  tick = () => this.setState({ time: new Date() });
  render() { return <div>{this.state.time.toLocaleTimeString()}</div>; }
}

// Function
function Clock() {
  const [time, setTime] = useState(new Date());
  useEffect(() => {
    const id = setInterval(() => setTime(new Date()), 1000);
    return () => clearInterval(id);
  }, []);
  return <div>{time.toLocaleTimeString()}</div>;
}

Phase order in a function render

  1. Body executes top-to-bottom, hooks register their slots.
  2. JSX is returned.
  3. React reconciles + commits.
  4. useLayoutEffect fires synchronously (blocks paint).
  5. Browser paints.
  6. useEffect fires async.

On the next render, cleanups from the previous render run BEFORE the new effects.

Things that don't map 1:1

  • componentWillReceiveProps / getDerivedStateFromProps: usually a smell. Prefer deriving in render or using a key to reset state.
  • forceUpdate: no direct equivalent — use a useReducer increment or a state flip.
  • Error boundaries: still require a class (no FC API yet). Wrap your FC tree.

Refs for instance variables

Anything you would have stored on this becomes a useRef:

tsx
const intervalRef = useRef<number | null>(null);

Mutating .current doesn't trigger a re-render — perfect for handles, IDs, accumulators.

Hidden lifecycle: each render is its own snapshot

State and props are captured at render time. A closure inside setTimeout sees the values from when it was created, not the latest values. This is the root cause of most 'stale closure' bugs — and why functional updaters (setX(prev => ...)) and refs exist as escape hatches.

Follow-up questions

  • What does StrictMode's double-mount reveal about effect lifecycle?
  • When would you reach for useLayoutEffect over useEffect?
  • How do you reset state when a prop changes — without an effect?

Common mistakes

  • Trying to replicate componentWillReceiveProps with useEffect — usually leads to bugs and infinite loops.
  • Storing non-reactive 'instance' values in useState — useRef is the right tool.
  • Reading state in a setTimeout that was set up on mount, expecting the latest value — stale closure.

Performance considerations

  • Effects are async (run after paint), so they don't block the browser. `useLayoutEffect` blocks paint — use sparingly. Class lifecycle methods had the same cost as their hook equivalents; the difference is ergonomic, not performance.

Edge cases

  • useEffect with no deps runs after every render — almost always a mistake.
  • Effects from unmounted components don't run, but pending fetches still resolve — guard with AbortController.
  • StrictMode dev double-mount: setup → cleanup → setup runs once on mount in development.

Real-world examples

  • Every modern React component is a function component. Class components survive mostly in older codebases and for error boundaries. New code: function + hooks, every time.

Senior engineer discussion

Senior framing: function components reframe lifecycle as 'what should happen when these dependencies change'. The deps array IS the lifecycle. Once that clicks, useEffect bugs largely go away.

Related questions