Skip to content

Component Rendering Lifecycle — Complete Guide

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Component Rendering Lifecycle. We cover key concepts, practical examples, and best practices to help you master this topic.

The component rendering lifecycle describes how components move from creation to DOM insertion to updates and finally removal, with hooks at each stage for custom logic.

What You'll Learn

  • The phases of a component's lifecycle
  • How the Virtual Dom interacts with lifecycle hooks
  • How React's useEffect maps to lifecycle phases
  • How to avoid common lifecycle pitfalls

Why It Matters

Lifecycle hooks control when side effects run, when subscriptions are set up, and when resources are cleaned up. Misunderstanding the lifecycle causes memory leaks, stale data, and rendering bugs.

flowchart LR
  A[Component Created] --> B[Render Phase]
  B --> C[Virtual DOM created]
  C --> D[Diffs with old tree]
  D --> E[Commit Phase]
  E --> F[DOM inserted/updated]
  F --> G[Lifecycle effects run]
  G --> H{Updates?}
  H -->|Yes| B
  H -->|No| I[Unmount Phase]
  I --> J[Cleanup effects]
  J --> K[DOM removed]

The Three Main Phases

Every component goes through mount, update, and unmount phases.

// Mount phase: component is created and inserted into the DOM
// 1. Constructor / initial render
// 2. Virtual DOM node created
// 3. Diffed (no previous tree, so all nodes are new)
// 4. Committed to DOM (Placement effects)
// 5. Lifecycle hooks fire (useEffect callbacks)

// Update phase: component re-renders due to state or prop changes
// 1. State change triggers re-render
// 2. New virtual tree created
// 3. Diffed against old tree
// 4. Patches committed to DOM
// 5. Cleanup of previous effects (if dependencies changed)
// 6. New effects fire

// Unmount phase: component is removed from the DOM
// 1. Component removed from virtual tree
// 2. Diff detects deletion
// 3. Cleanup effects run
// 4. DOM node removed

Building a Lifecycle-Aware Virtual DOM

Track component states through the lifecycle.

// Lifecycle states
const Lifecycle = {
    CREATED: 'created',
    MOUNTING: 'mounting',
    MOUNTED: 'mounted',
    UPDATING: 'updating',
    UPDATED: 'updated',
    UNMOUNTING: 'unmounting',
    UNMOUNTED: 'unmounted'
};

class LifecycleComponent {
    constructor(props) {
        this.props = props;
        this.state = {};
        this._lifecycle = Lifecycle.CREATED;
        this._effectCleanups = [];
        this._vnode = null;
    }

    // Called by the framework during render phase
    render() {
        // Building virtual tree
        return this._render();
    }

    _render() {
        return {
            type: 'div',
            props: {},
            children: ['Hello ' + this.props.name]
        };
    }

    // Called by the framework after DOM commit
    _didMount() {
        this._lifecycle = Lifecycle.MOUNTED;
        // Run effects
        this._runEffects();
    }

    // Called by the framework when props/state change
    _willUpdate(nextProps, nextState) {
        this._lifecycle = Lifecycle.UPDATING;
    }

    _didUpdate(prevProps, prevState) {
        this._lifecycle = Lifecycle.UPDATED;
        // Cleanup old effects
        this._cleanupEffects();
        // Run new effects
        this._runEffects();
    }

    // Called by the framework before DOM removal
    _willUnmount() {
        this._lifecycle = Lifecycle.UNMOUNTING;
        this._cleanupEffects();
        this._lifecycle = Lifecycle.UNMOUNTED;
    }

    _runEffects() {
        // Effects registered via useEffect equivalent
        this._effects.forEach(effect => {
            const cleanup = effect();
            if (typeof cleanup === 'function') {
                this._effectCleanups.push(cleanup);
            }
        });
    }

    _cleanupEffects() {
        this._effectCleanups.forEach(cleanup => cleanup());
        this._effectCleanups = [];
    }
}

React Hooks Mapped to Lifecycle

React hooks map to specific lifecycle points.

import React, { useState, useEffect, useLayoutEffect, useRef } from 'react';

function LifecycleDemo({ userId }) {
    const [data, setData] = useState(null);
    const prevUserIdRef = useRef();

    // useEffect runs after the browser paints (async)
    useEffect(() => {
        // This runs after EVERY completed render
        // where [userId] changed
        console.log('After paint: userId changed');

        // Simulate data fetching
        fetch('/api/users/' + userId)
            .then(r => r.json())
            .then(data => setData(data));

        // Cleanup function runs:
        // 1. Before the next effect run (if userId changes)
        // 2. On unmount
        return () => {
            console.log('Cleanup: abort fetch or unsubscribe');
            // AbortController would go here
        };
    }, [userId]);

    // useLayoutEffect runs synchronously after DOM mutations
    // but before the browser paints
    useLayoutEffect(() => {
        // Use this for measuring DOM or preventing visual flicker
        // Similar to componentDidMount/componentDidUpdate
        console.log('Before paint: DOM updated');
    }, [userId]);

    // Ref stores previous value across renders
    useEffect(() => {
        prevUserIdRef.current = userId;
    });

    const prevUserId = prevUserIdRef.current;

    return (
        <div>
            <p>Current: {userId}</p>
            <p>Previous: {prevUserId}</p>
            <pre>{JSON.stringify(data, null, 2)}</pre>
        </div>
    );
}

Lifecycle and the Virtual DOM

The reconciler manages lifecycle transitions based on virtual tree comparison.

// The reconciler decides lifecycle transitions from diff results
function reconcileFiber(fiber, newVNode) {
    if (fiber === null) {
        // New fiber: MOUNT lifecycle
        return mountFiber(newVNode);
    }

    if (newVNode === null) {
        // Fiber exists but no new vnode: UNMOUNT lifecycle
        fiber.effectTag = 'DELETION';
        return null;
    }

    if (fiber.type !== newVNode.type) {
        // Different type: UNMOUNT old, MOUNT new
        fiber.effectTag = 'DELETION';
        return mountFiber(newVNode);
    }

    // Same type: UPDATE lifecycle
    fiber.effectTag = 'UPDATE';
    fiber.pendingProps = newVNode.props;

    // Children reconciliation determines
    // child component lifecycle transitions
    reconcileChildren(fiber, newVNode.props.children);

    return fiber;
}

// After all fibers are processed, the commit phase
// walks the effect list and calls lifecycle hooks:
// 1. For DELETION fibers: componentWillUnmount
// 2. For PLACEMENT fibers: componentDidMount
// 3. For UPDATE fibers: componentDidUpdate

Common Lifecycle Pitfalls

Avoid these common mistakes with lifecycle hooks.

// Pitfall 1: Missing cleanup causes memory leaks
useEffect(() => {
    const timer = setInterval(() => {
        console.log('Tick');
    }, 1000);
    // Missing cleanup: timer keeps running after unmount
    // return () => clearInterval(timer);  // FIX
}, []);

// Pitfall 2: Stale closure in effects
useEffect(() => {
    // This closure captures the value of count at effect creation time
    // If count changes, the interval still uses the old value
    const timer = setInterval(() => {
        console.log(count);  // Always logs the initial count
    }, 1000);
    return () => clearInterval(timer);
}, []);  // Empty dependency array: effect never re-runs
// FIX: Add count to dependencies or use ref

// Pitfall 3: Infinite update loops
useEffect(() => {
    // This effect sets state, which triggers re-render,
    // which triggers this effect again... infinite loop
    setData({ ...data, updated: true });
}, [data]);  // data changes every render
// FIX: Use functional update or remove data from deps

// Pitfall 4: Wrong lifecycle for DOM measurements
useEffect(() => {
    // useEffect runs AFTER paint
    // Measuring DOM here shows the POST-PAINT state
    const width = elementRef.current.offsetWidth;
    // This causes a forced reflow after paint — wasteful
}, []);

// FIX: useLayoutEffect for DOM measurements
useLayoutEffect(() => {
    // Runs BEFORE paint, measurement happens before browser renders
    const width = elementRef.current.offsetWidth;
}, []);

Common Mistakes

  1. Running side effects in the render phase (should only happen in commit phase).
  2. Forgetting cleanup functions in useEffect, causing memory leaks.
  3. Using useEffect for synchronous DOM measurements (use useLayoutEffect instead).
  4. Having stale closures by omitting dependencies from useEffect arrays.
  5. Setting state in useEffect without proper dependency management, causing infinite loops.

Practice Questions

  1. What are the three main lifecycle phases? Mount, update, and unmount.
  2. When does useEffect run? After the browser paints (asynchronously).
  3. When does useLayoutEffect run? Synchronously after DOM mutations but before the browser paints.
  4. What happens during the unmount phase? Cleanup functions run, then the DOM node is removed.

Challenge

Build a component lifecycle visualizer that shows the current phase of each component in a tree. Use different colors for mount (green), update (yellow), and unmount (red). Animate the state transitions. Include lifecycle timing information (render time, commit time, effect duration).

FAQ

What is the component lifecycle?

The component lifecycle is the series of phases a component goes through: creation, DOM insertion, updates, and removal.

What is the difference between useEffect and useLayoutEffect?

useEffect runs after paint (async). useLayoutEffect runs before paint (sync). Use useLayoutEffect for DOM measurements to avoid flicker.

Why are cleanup functions important in useEffect?

Cleanup prevents memory leaks by canceling subscriptions, timers, and aborting fetch requests before the next effect runs or on unmount.

Can I use lifecycle hooks in function components?

Yes. useEffect, useLayoutEffect, and useInsertionEffect cover mount, update, and unmount scenarios in function components.

What is a stale closure in useEffect?

A stale closure captures an outdated variable value because the effect doesn't re-run when that variable changes. Fix by adding the variable to the dependency array or using a ref.

Mini Project

Build a set of lifecycle demo components. Create components using class-based (componentDidMount, componentDidUpdate, componentWillUnmount) and hook-based (useEffect, useLayoutEffect) lifecycles. Show a timeline of when each hook fires relative to the render and paint cycles.

What's Next

Lesson 10: Virtual DOM in React

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro