Understanding React’s Component Lifecycle (Hooks Way)



This content originally appeared on DEV Community and was authored by Ali Aslam

Every React component has a story.

It’s born (when it first appears on the screen), it lives (responding to user input, fetching data, updating the UI), and eventually, it retires (when React says, “Thanks for your service” and removes it from the DOM).

This journey is called the component lifecycle — and understanding it isn’t just trivia. It’s the secret to:

  • Fetching data at the right moment (and only once)
  • Avoiding awkward bugs like double API calls or memory leaks
  • Writing components that feel smooth, not sluggish

Now, don’t worry — we’re not going back to the class-component days of memorizing things like componentDidMount or componentWillUnmount. Those still exist historically, but we’re in a hooks-first world, so we’ll learn lifecycle concepts and map them to modern function components.

Table of Contents

Foundations

  • What Do We Mean by “Lifecycle”?
  • The Three Big Stages

    • Mounting (Birth)
    • Updating (Life)
    • Unmounting (Retirement)

Lifecycle in Modern React

  • Lifecycle in the Hooks Era

    • Mapping stages to hooks
  • Mounting Phase — Birth of a Component

    • Key takeaways for mount effects
    • When to use useLayoutEffect on mount

Updating & Unmounting

  • Updating Phase — The Component Grows and Changes

    • What triggers updates
    • Running code on updates with deps
    • Avoiding infinite loops
  • Unmounting Phase — The Goodbye

    • Cleaning up with effect return values
    • Common cleanup tasks

Advanced Lifecycle Nuances

  • Concurrent Rendering — React’s Multitasking Superpower

    • Render ≠ commit
    • Writing effects for concurrent safety
    • Stale data cancellation pattern
  • React 19, Strict Mode, and Double Invocations

    • Why dev double-invokes mount/unmount
    • How it catches missing cleanup

Applying Lifecycle Knowledge

  • Common Real-World Patterns

    • Data fetching without chaos
    • DOM measurement patterns
    • Animation start/cleanup
    • Subscriptions & timers
    • Optimistic UI updates & rollback
  • Debugging Lifecycle Bugs

    • Infinite loop detection
    • Memory leak fixes
    • Strict Mode double-run explanation
    • Using React DevTools Profiler
    • 5. ESLint’s Hooks Rules Are Not Annoying (They’re Life-Saving)

Wrap-Up

  • Conclusion & Key Takeaways
  • Next Up: React 19 Event Handling — From onClick to useEvent

What Do We Mean by “Lifecycle”?

When React talks about “component lifecycle,” it’s really talking about moments in time when React interacts with your component.

Think of it like checkpoints:

  1. When the component first shows up (React mounts it to the DOM)
  2. When something changes (React re-renders and updates the DOM)
  3. When it’s removed (React unmounts it from the DOM)

These checkpoints are when you, the developer, get a chance to run your own code: fetching data, subscribing to events, measuring the DOM, cleaning up resources, and more.

In class components, React gave you methods for each stage.
In function components, we have hooks — especially useEffect and useLayoutEffect — to tie into these lifecycle moments.

The Three Big Stages

You can break down the lifecycle into three major phases:

Mounting (Birth)

  • This is when the component is created and added to the DOM for the first time.
  • It’s your chance to set things up: start API requests, attach event listeners, initialize animations.
  • In hooks, this usually means useEffect with an empty dependency array — it runs after the first render only.

Updating (Life)

  • Happens whenever state or props change.
  • React calls your component function again to calculate the new UI, then updates only what changed in the DOM.
  • This is where you react (pun intended) to changes — e.g., refetching data if a query parameter changes.
  • In hooks, useEffect with specific dependencies runs during updates when those dependencies change.

Unmounting (Retirement)

  • This is when React removes your component from the DOM.
  • It’s cleanup time: stop timers, remove event listeners, abort network requests.
  • In hooks, useEffect cleanup functions run here — think of it as your modern componentWillUnmount.

💡 Why this matters: If you know when each stage happens, you can write code that’s predictable, efficient, and bug-free. If you don’t know, you might end up with triple API calls, stale data, or performance issues you can’t explain.

Next let’s map these stages to actual React hooks, then explore the subtle but important differences between “classic lifecycle” thinking and “modern hooks” thinking. And later, we’ll even peek into React 19’s quirks like double-invoked mounts in Strict Mode and concurrent rendering.

Lifecycle in the Hooks Era

React’s mental model has changed a lot since hooks landed in 16.8.
The old way: “Which lifecycle method should I use here?”
The new way: “When should this effect run?”

Instead of a bunch of lifecycle methods, we now have a few powerful hooks that can cover all stages if you use them right:

  • useEffect — The all-rounder for running side effects after React updates the DOM.
  • useLayoutEffect — Similar to useEffect, but runs synchronously after DOM updates, before the browser paints.
  • useMemo / useCallback — Not side effect hooks per se, but tied to the render/update rhythm for performance optimization.
  • Custom hooks — Your way to package lifecycle logic into reusable units.

The key difference:
Hooks don’t “fire” at specific named lifecycle points like class methods. Instead, they run after each render, and you control when they run again via dependency arrays.

So, mapping the three stages:

Lifecycle Stage Hook Usage
Mounting useEffect(() => { ... }, [])
Updating useEffect(() => { ... }, [someDependency])
Unmounting Cleanup function inside your effect

Mounting Phase — Birth of a Component

This is the moment your component steps into the world.

Example: You load a profile page and React mounts <UserProfile />. The DOM gets built, styles applied, and you have a fresh, shiny component ready to run code.

In hooks, a “mount effect” looks like this:

import { useEffect } from "react";

function UserProfile({ userId }) {
  useEffect(() => {
    console.log("Component mounted 🚀");

    // Fetch user data
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => console.log("User data:", data));

  }, []); // Empty array = only run on mount

  return <div>Loading user...</div>;
}

Key takeaways for mount effects

  • The empty dependency array ([]) means “run only after first render.”
  • Perfect for one-time setup: fetching data, setting up event listeners, subscribing to a service.
  • ⚠ Don’t forget cleanup if your setup needs it — otherwise, you risk leaks when the component unmounts.

When to Use useLayoutEffect on Mount

Most of the time, useEffect is fine. But sometimes you need the DOM to be measured or manipulated before the browser paints, so the user never sees a “jump.”

Example:

useLayoutEffect(() => {
  const height = document.getElementById("box").offsetHeight;
  console.log("Box height before paint:", height);
}, []);

Use cases for mount-time useLayoutEffect:

  • Measuring DOM dimensions for animations
  • Reading layout before making visual adjustments
  • Avoiding “flash of wrong layout”

✅ Pro Tip: Even though mounting effects feel like they only run once, remember that in React’s Strict Mode (in dev), React might mount, unmount, and mount your component twice to help you catch bugs — we’ll get deep into that later.

Updating Phase — The Component Grows and Changes

Mounting is the “Hello World 👋” moment, but most components don’t stay static. Props change, state changes, and React re-renders the component with new data. This is the updating phase — where things get interesting… and where a lot of bugs like to hang out.

When Does an Update Happen?

Your component will update when:

  • Props change — e.g., your <UserProfile userId={42} /> suddenly gets a new userId
  • State changes — you call a state updater like setCount(count + 1)
  • Context changes — any value from a useContext provider changes

Running Code on Updates

The most common way: add dependencies to your useEffect.

import { useEffect, useState } from "react";

function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    console.log("Query changed, fetching new results…");

    fetch(`/api/search?q=${query}`)
      .then(res => res.json())
      .then(data => setResults(data));

  }, [query]); // Runs whenever `query` changes

  return <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>;
}

Here’s what’s happening:

  • The effect runs after every render if query is different from the last render.
  • This is how you “hook into” the update lifecycle moment in React.

Avoiding Infinite Loops 🔄

Biggest gotcha: accidentally putting something in the dependency array that changes inside the effect.

Bad example:

useEffect(() => {
  setCount(count + 1); // will re-trigger effect forever
}, [count]);

Fixes:

  • Only update state inside effects when it’s actually needed
  • Sometimes use a functional update:
setCount(prev => prev + 1);

Unmounting Phase — The Goodbye

All good things must come to an end. The unmounting phase happens when React removes a component from the DOM — maybe because the user navigated away, or because its parent decided it’s no longer needed.

Unmounting is where you clean up after yourself so you don’t leave behind memory leaks, stale listeners, or zombie intervals.

Cleaning Up with Effects

Every effect can optionally return a cleanup function.
React calls this cleanup before the component unmounts — and also before the next time the effect re-runs.

useEffect(() => {
  const handler = () => console.log("Window resized!");
  window.addEventListener("resize", handler);

  // Cleanup
  return () => {
    window.removeEventListener("resize", handler);
    console.log("Cleanup done 🧹");
  };
}, []); // Runs only on mount/unmount

Common Cleanup Tasks:

  • Removing event listeners
  • Canceling network requests
  • Clearing timers/intervals
  • Disconnecting from WebSocket connections

✅ Pro Tip: In Strict Mode (development only), React actually mounts and unmounts your component right away after the first mount to ensure your cleanup logic works correctly. If you see effects running twice in dev but not prod — that’s why.

Concurrent Rendering — React’s Multitasking Superpower

Think of the old React rendering model like a chef who makes one dish at a time: once they start, they can’t stop until it’s done.

Concurrent Rendering is more like a chef who can pause making your sandwich, work on someone’s pasta, then come back — all without losing their place.

Why This Matters for Lifecycle

In concurrent mode, React might:

  • Start rendering your component but pause before committing it to the DOM
  • Abandon a render halfway if newer data comes in
  • Render multiple versions of your UI in memory before deciding which to show

Key point: Just because a component rendered doesn’t mean it will be committed. Your effects only run after commit — so if a render is abandoned, the effect never happens.

How to Write Effects for Concurrent Safety

  1. Avoid doing irreversible work in render (like starting a fetch). Use useEffect for side effects.
  2. Check for stale data in async callbacks — because the UI might have moved on.
  3. Make sure cleanup functions can run even if your effect never “finishes” in the way you expect.

Example: Stale Data Problem

useEffect(() => {
  let cancelled = false;

  fetch(`/api/data?q=${query}`)
    .then(res => res.json())
    .then(data => {
      if (!cancelled) setData(data);
    });

  return () => {
    cancelled = true; // Stop outdated updates
  };
}, [query]);

This avoids trying to update state after the component has unmounted or the effect has been replaced.

React 19, Strict Mode, and Double Invocations

If you’ve ever logged something inside an effect in development and thought:

“Wait… why is this running twice?!”

That’s Strict Mode doing its job.

What’s Happening

In development mode only (not production), Strict Mode:

  1. Mounts your component
  2. Immediately unmounts it
  3. Mounts it again

This double invocation is intentional — it flushes out bugs where cleanup is missing or effects aren’t idempotent (safe to run multiple times without breaking things).

Why This Is Great (Even If It Feels Annoying)

Without this, you might ship a memory leak or dangling subscription to production without noticing.

Example:

useEffect(() => {
  const id = setInterval(() => console.log("tick"), 1000);

  return () => clearInterval(id); // Without this, you'd leak intervals
}, []);

If you forget the cleanup, Strict Mode’s double mount/unmount will make it obvious — you’ll see multiple intervals running in dev.

✅ Pro Tip: Always make your effects idempotent — meaning if they run twice in a row, nothing bad happens. This habit will make your code concurrent-safe by default.

Common Real-World Patterns — Lifecycle in Action

Knowing the React lifecycle isn’t just trivia — it’s a superpower. Once you get how components mount, update, and unmount, you start writing code that feels… inevitable. Everything happens when it’s supposed to. No surprises.

Let’s look at some patterns you’ll actually use in production — and exactly where in the lifecycle they fit.

1. Data Fetching Without the Chaos

You want to fetch data when a component mounts, but also refetch when a dependency changes — and not refetch if the component is in the middle of unmounting.

useEffect(() => {
  let ignore = false;

  fetch(`/api/posts?category=${category}`)
    .then(res => res.json())
    .then(data => {
      if (!ignore) setPosts(data);
    });

  return () => { ignore = true; };
}, [category]);

Lifecycle moment:

  • Runs after mount (initial fetch)
  • Runs again after updates to category
  • Cleans up if the component unmounts before the fetch completes

2. DOM Measurements That Don’t Lie

If you need to measure an element’s size, you can’t wait until after the browser paints — you need it immediately after the DOM is updated.

const ref = useRef();

useLayoutEffect(() => {
  console.log(ref.current.getBoundingClientRect());
}, []);

Lifecycle moment:

  • Runs after DOM mutations but before the browser paints
  • Guarantees you’re measuring the fresh, updated DOM

3. Animations That Start at the Right Time

Want to start an animation right when a component appears, and clean it up when it disappears?

useEffect(() => {
  const el = document.querySelector(".fade-in");
  el.classList.add("visible");

  return () => el.classList.remove("visible");
}, []);

Lifecycle moment:

  • Starts after mount so the animation can transition from the initial state
  • Cleans up so you don’t leave “visible” classes on detached DOM nodes

4. Cleaning Up Subscriptions and Timers

Subscriptions, sockets, intervals — if you set them up without cleanup, you’re basically giving your app a memory leak.

useEffect(() => {
  const id = setInterval(() => console.log("tick"), 1000);

  return () => clearInterval(id);
}, []);

Lifecycle moment:

  • Subscribes on mount
  • Unsubscribes on unmount (or before next effect run)

5. Optimistic UI Updates

Sometimes you update the UI before the server confirms — but you also need a rollback plan.

function handleLike() {
  setLikes(likes + 1); // Optimistic update

  fetch("/api/like", { method: "POST" })
    .catch(() => setLikes(likes)); // Rollback if it fails
}

Lifecycle moment:

  • Update happens instantly on user interaction
  • Cleanup/rollback happens if async operation fails

🔑 Key takeaway: Lifecycle awareness makes your effects intentional — you know exactly when they’ll run, why, and how to clean up. Without that, you’re just rolling the dice and hoping React does what you expect.

Debugging Lifecycle Bugs

Even if you think you know the lifecycle inside-out, React will occasionally throw you a curveball. Infinite loops. Missing cleanups. “Why is this effect running twice?!” moments.

Let’s talk about the usual suspects — and how to catch them before they burn down your sanity.

1. Infinite Loop Detection

Symptom: Your app freezes, your fan sounds like a jet engine, and your console is vomiting the same log over and over.

Why it happens: An effect changes state, which re-runs the effect, which changes state… and so on forever.

useEffect(() => {
  setCount(count + 1); // 🚨 This will loop forever
}, [count]);

Fix:

  • Only update state if it’s actually needed
  • Consider using a conditional or refactoring logic outside the effect
useEffect(() => {
  if (count < 5) setCount(c => c + 1);
}, [count]);

2. Memory Leaks from Missing Cleanup

Symptom: Warnings like “Can’t perform a React state update on an unmounted component” or steadily climbing memory usage.

Why it happens: Async calls or subscriptions keep running after the component unmounts.

Fix: Always return a cleanup function in useEffect.

useEffect(() => {
  const id = setInterval(() => console.log("tick"), 1000);

  return () => clearInterval(id); // ✅ cleanup
}, []);

3. “Why Is My Effect Running Twice?!” in Strict Mode

Symptom: Effects fire twice in development but not in production.

Why it happens: React’s Strict Mode double-invokes mount/unmount sequences in dev to help catch unsafe logic.

Fix:

  • Understand it’s intentional — don’t hack around it
  • Make effects resilient to being run more than once
  • Use a ref flag if you truly need to run something once in dev

4. React DevTools Profiler

Your best friend for lifecycle mystery cases. It shows:

  • Which components are re-rendering
  • Why they re-rendered (props change? state change?)
  • How long each render took

Pro tip: Filter out tiny components so you can focus on the big hitters.

5. ESLint’s Hooks Rules Are Not Annoying (They’re Life-Saving)

Symptom: You ignore that little red underline in your editor and everything explodes later.

Why it happens: Missing dependencies in an effect means your code is relying on stale values.

Fix: Install and respect the eslint-plugin-react-hooks rules. They will force you to think about why a dependency is (or isn’t) there.

🔧 Key takeaway: Most lifecycle bugs aren’t React being unpredictable — they’re effects not being written with their full lifecycle in mind. Write them with mounting, updating, and unmounting all accounted for, and you’ll avoid most headaches.

Conclusion 🎯

The React component lifecycle isn’t just a theoretical diagram to memorize — it’s the playbook for how your components live, breathe, and eventually clean up after themselves.

By understanding:

  • When React mounts, updates, and unmounts
  • How concurrent rendering and Strict Mode affect those moments
  • Common patterns like data fetching and animation hooks
  • Debugging tools and best practices

…you’re now better equipped to write code that’s not only functional but predictable, performant, and bug-resistant.

Instead of wrestling with mysterious side effects and random re-renders, you’ll start anticipating them — and maybe even impressing your future self with “oh yeah, I accounted for that” moments.

Next up in the React Deep Dive series:
⚡ * React 19 Event Handling — From onClick to useEvent* — e’ll explore how React’s event system works under the hood, what’s changing in React 19, and how to write event code that’s both cleaner and more performant.

Follow me on DEV for future posts in this deep-dive series.
https://dev.to/a1guy
If it helped, leave a reaction (heart / bookmark) — it keeps me motivated to create more content
Want video demos? Subscribe on YouTube: @LearnAwesome


This content originally appeared on DEV Community and was authored by Ali Aslam