Back to Blog
React Hooks Execution Order & Rendering Mechanism Deep Dive
📝 Dev Notes

React Hooks Execution Order & Rendering Mechanism Deep Dive

B
Blake
Dec 16, 2025 By Blake 30 min read
Understanding React's rendering mechanism is critical for writing efficient applications. This article systematically analyzes the execution timing of useEffect and useLayoutEffect, their appropriate use cases, and React 18's new batching mechanism. Mastering these concepts will help you avoid common performance issues and UI flickering.

Core Concept: Two Phases of React Rendering

Render Phase

During this phase:

  • Virtual DOM changes are computed

  • Function Component body is executed

  • Pure computational operations with no side effects

  • Can be interrupted by React in Concurrent Mode

Commit Phase

During this phase:

  • Changes are applied to the real DOM

  • Effect Hooks are executed (useEffect, useLayoutEffect)

  • Cannot be interrupted — must execute completely

Visual Process Flow

┌─────────────────────────────────────────────────┐
│           Render Phase                          │
│  1. Execute Function Component                  │
│  2. Compute Virtual DOM diff                    │
│  3. Prepare content to update                   │
└─────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────┐
│           Commit Phase                          │
│  1. DOM Mutation (update real DOM)              │
│  2. useLayoutEffect (synchronous execution)    │
│  3. Browser Paint (screen update)               │
│  4. useEffect (asynchronous execution)         │
└─────────────────────────────────────────────────┘

Three Effect Hooks: Execution Timing Breakdown

Complete Execution Order

Component Render
      ↓
┌──────────────┐
│  DOM Update  │ ← Update real DOM
└──────────────┘
      ↓
┌────────────────────┐
│ useLayoutEffect    │ ← Synchronous, BLOCKS paint
│ (Synchronous)      │
└────────────────────┘
      ↓
┌──────────────┐
│ Browser Paint│ ← User sees updated screen
└──────────────┘
      ↓
┌────────────────────┐
│ useEffect          │ ← Asynchronous, doesn't affect paint
│ (Asynchronous)     │
└────────────────────┘

1. useEffect — For Most Scenarios

Execution Timing: After browser paint completes
Characteristic: Asynchronous execution, doesn't block rendering
Usage Frequency: ~99% of cases

useEffect(() => {
  // Executes after browser paint
  // Does not block screen rendering
}, [dependencies]);

Use Cases:

  • Data fetching (API calls)

  • Event subscription

  • Setting up timers

  • Analytics tracking

Code Example:

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(err => {
        console.error(err);
        setLoading(false);
      });
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  return <div>{user?.name}</div>;
}

2. useLayoutEffect — Synchronous DOM Measurement & Adjustment

Execution Timing: After DOM update, before paint, executes synchronously
Characteristic: Blocks screen rendering until Hook completes
Usage: When synchronous DOM measurement or modification is needed

useLayoutEffect(() => {
  // Executes after DOM update, before paint (synchronous)
  // BLOCKS screen rendering
}, [dependencies]);

Use Cases:

  • Measuring DOM element dimensions (width, height, scrollHeight)

  • Adjusting DOM based on measurements

  • Preventing UI flicker

  • Dynamic tooltip or popover positioning

Code Example:

interface TooltipProps {
  targetRef: React.RefObject<HTMLElement>;
  children: React.ReactNode;
}

function Tooltip({ targetRef, children }: TooltipProps) {
  const tooltipRef = useRef<HTMLDivElement>(null);
  const [position, setPosition] = useState({ top: 0, left: 0 });

  useLayoutEffect(() => {
    if (targetRef.current && tooltipRef.current) {
      const targetRect = targetRef.current.getBoundingClientRect();
      const tooltipRect = tooltipRef.current.getBoundingClientRect();

      setPosition({
        top: targetRect.bottom + 8,
        left: targetRect.left + (targetRect.width - tooltipRect.width) / 2,
      });
    }
  }, [targetRef]);

  return (
    <div
      ref={tooltipRef}
      style={{
        position: 'fixed',
        top: position.top,
        left: position.left,
      }}
    >
      {children}
    </div>
  );
}

3. useInsertionEffect — CSS-in-JS Specific (React 18+)

Execution Timing: Before any DOM mutation occurs
Target Users: CSS-in-JS libraries (emotion, styled-components, etc.)
General Developers: Rarely need this

useInsertionEffect(() => {
  // Executes before any DOM mutation
  // Exclusively for CSS-in-JS libraries
}, [dependencies]);

Execution Order:

useInsertionEffect → DOM Update → useLayoutEffect → Paint → useEffect

Practical Comparison: useEffect vs useLayoutEffect

The Flickering Problem

Problem with useEffect:

function FlickeringComponent() {
  const [height, setHeight] = useState(0);
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    // ❌ Executes after paint
    // User first sees height=0, then correct height
    if (ref.current) {
      setHeight(ref.current.scrollHeight);
    }
  }, []);

  return (
    <div>
      <div ref={ref} style={{ height }}>
        Content with dynamic height
      </div>
    </div>
  );
}

Solution with useLayoutEffect:

function NoFlickerComponent() {
  const [height, setHeight] = useState(0);
  const ref = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {
    // ✅ Executes before paint
    // User only sees final correct state
    if (ref.current) {
      setHeight(ref.current.scrollHeight);
    }
  }, []);

  return (
    <div>
      <div ref={ref} style={{ height }}>
        Content with dynamic height
      </div>
    </div>
  );
}

Execution Timing Verification

function TimingDemo() {
  console.log('1. Component render');

  useInsertionEffect(() => {
    console.log('2. useInsertionEffect');
  });

  useLayoutEffect(() => {
    console.log('4. useLayoutEffect');
  });

  useEffect(() => {
    console.log('5. useEffect');
  });

  return <div ref={() => console.log('3. DOM ref callback')}>Content</div>;
}

// Expected console output order:
// 1. Component render
// 2. useInsertionEffect
// 3. DOM ref callback
// 4. useLayoutEffect
// 5. useEffect

useState Batching Mechanism

React 18 Automatic Batching

React 18 automatically batches multiple setState calls into a single re-render by default.

function BatchingDemo() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);
  const [text, setText] = useState('');

  console.log('Render!'); // Only executes once

  const handleClick = () => {
    // React 18: These three updates are batched, triggers only one re-render
    setCount(c => c + 1);
    setFlag(f => !f);
    setText('Updated');
  };

  // Even inside setTimeout, batching applies (React 18 feature)
  const handleAsync = () => {
    setTimeout(() => {
      setCount(c => c + 1);
      setFlag(f => !f);
      // React 18: Still triggers only one re-render
      // React 17: Would trigger two re-renders
    }, 0);
  };

  return (
    <div>
      <p>Count: {count}, Flag: {String(flag)}, Text: {text}</p>
      <button onClick={handleClick}>Sync Update</button>
      <button onClick={handleAsync}>Async Update</button>
    </div>
  );
}

Functional Updates vs Direct Updates

Wrong Approach — Sequential Direct Updates:

const incrementWrong = () => {
  setCount(count + 1); // count is 0, set to 1
  setCount(count + 1); // count still 0 (closure), set to 1
  setCount(count + 1); // count still 0 (closure), set to 1
  // Final result: 1 (expected 3)
};

Correct Approach — Functional Updates:

const incrementRight = () => {
  setCount(c => c + 1); // 0 + 1 = 1
  setCount(c => c + 1); // 1 + 1 = 2
  setCount(c => c + 1); // 2 + 1 = 3
  // Final result: 3 ✓
};

Force Synchronous Update

Some scenarios require immediate DOM updates:

import { flushSync } from 'react-dom';

function FlushSyncDemo() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  const handleClick = () => {
    flushSync(() => {
      setCount(c => c + 1);
    });
    // DOM is updated

    flushSync(() => {
      setFlag(f => !f);
    });
    // DOM is updated again

    // Total: 2 re-renders (instead of default 1)
  };

  return <div>{count} - {String(flag)}</div>;
}

Class Component Lifecycle Mapping

Class Component

Function Component

Description

constructor

useState initial value

Initialize state

getDerivedStateFromProps

Compute in render or useMemo

Calculate based on props

shouldComponentUpdate

React.memo or useMemo

Determine if re-render

render

Function Component body

Define UI structure

componentDidMount

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

Execute after first mount

componentDidUpdate

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

Execute after update

componentWillUnmount

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

Cleanup before unmount

getSnapshotBeforeUpdate

useLayoutEffect

Capture state before DOM

componentDidCatch

Error Boundary (class needed)

Error handling

Key Differences

// Class Component — Manual prop comparison
class ClassExample extends React.Component {
  componentDidMount() {
    // Executes once after initial mount
  }

  componentDidUpdate(prevProps) {
    // Executes after every update
    if (prevProps.id !== this.props.id) {
      // Manual prop change detection
    }
  }
}

// Function Component — Automatic dependency tracking
function FunctionExample({ id }: { id: string }) {
  useEffect(() => {
    // Executes on initial mount + whenever id changes
    // Dependency automatically tracked
  }, [id]);

  useEffect(() => {
    // Executes only on initial mount
  }, []);

  useEffect(() => {
    // Executes after every render (usually not recommended)
  });
}

Real-World Examples

Example 1: Auto-Resizing Textarea

function AutoResizeTextarea() {
  const textareaRef = useRef<HTMLTextAreaElement>(null);
  const [value, setValue] = useState('');

  useLayoutEffect(() => {
    const textarea = textareaRef.current;
    if (textarea) {
      textarea.style.height = 'auto';
      textarea.style.height = `${textarea.scrollHeight}px`;
    }
  }, [value]);

  return (
    <textarea
      ref={textareaRef}
      value={value}
      onChange={(e) => setValue(e.target.value)}
      style={{ overflow: 'hidden', resize: 'none' }}
    />
  );
}

Example 2: Animated List Items

function AnimatedList({ items }: { items: string[] }) {
  const [prevItems, setPrevItems] = useState<string[]>([]);
  const [animatingItems, setAnimatingItems] = useState<Set<string>>(new Set());

  useLayoutEffect(() => {
    const newItems = items.filter(item => !prevItems.includes(item));

    if (newItems.length > 0) {
      setAnimatingItems(new Set(newItems));

      requestAnimationFrame(() => {
        requestAnimationFrame(() => {
          setAnimatingItems(new Set());
        });
      });
    }

    setPrevItems(items);
  }, [items, prevItems]);

  return (
    <ul>
      {items.map(item => (
        <li
          key={item}
          className={animatingItems.has(item) ? 'animate-in' : ''}
        >
          {item}
        </li>
      ))}
    </ul>
  );
}

Example 3: Modal Focus Management

function Modal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
  const modalRef = useRef<HTMLDivElement>(null);
  const previousActiveElement = useRef<Element | null>(null);

  useLayoutEffect(() => {
    if (isOpen) {
      previousActiveElement.current = document.activeElement;
      modalRef.current?.focus();
    } else {
      (previousActiveElement.current as HTMLElement)?.focus();
    }
  }, [isOpen]);

  if (!isOpen) return null;

  return (
    <div
      ref={modalRef}
      tabIndex={-1}
      role="dialog"
      aria-modal="true"
    >
      <h2>Modal Title</h2>
      <button onClick={onClose}>Close</button>
    </div>
  );
}

Performance Considerations & Best Practices

Hook Impact on Paint

Hook

Blocks Paint

Performance Impact

Use Case

useInsertionEffect

Yes

Severe

CSS-in-JS libraries

useLayoutEffect

Yes

Moderate

DOM measurement, prevent flicker

useEffect

No

Minimal

Most side effects

Best Practices

// ✓ Good — Use useEffect for most scenarios
useEffect(() => {
  fetchData();
  subscribeToEvents();
}, []);

// ✓ Good — Use useLayoutEffect for synchronous DOM operations
useLayoutEffect(() => {
  measureElement();
  adjustPosition();
}, []);

// ✗ Bad — Time-consuming operations in useLayoutEffect
useLayoutEffect(() => {
  // This blocks paint and causes UI lag!
  heavyCalculation();
  fetchData();
}, []);

// ✗ Bad — Missing dependency array
useEffect(() => {
  // Executes after every render, causing performance issues
  setupListener();
  return () => removeListener();
});

// ✓ Good — Explicit dependency specification
useEffect(() => {
  setupListener();
  return () => removeListener();
}, [dependency]);

Common Interview Questions & Answers

Q1: What are the main differences between useEffect and useLayoutEffect?

Aspect

useEffect

useLayoutEffect

Execution Timing

After paint (asynchronous)

Before paint (synchronous)

Blocks Paint

No

Yes

Usage Frequency

~99%

~1%

Use Cases

API, subscriptions, timers

DOM measurement, prevent flicker

Performance Impact

Minimal

Potential visual delay

Q2: Why does calling setCount(count + 1) three times only increment by 1?

Reason: Closure causes all calls to receive the same count value.

// ❌ Wrong
const incrementWrong = () => {
  setCount(count + 1);  // 0 + 1 = 1
  setCount(count + 1);  // 0 + 1 = 1
  setCount(count + 1);  // 0 + 1 = 1
};

// ✓ Correct — Use functional updates
const incrementRight = () => {
  setCount(c => c + 1); // 0 + 1 = 1
  setCount(c => c + 1); // 1 + 1 = 2
  setCount(c => c + 1); // 2 + 1 = 3
};

Q3: When does UI flickering occur? How do you fix it?

Problem: useEffect executes after paint, so user first sees old state, then new state.

Solution: Use useLayoutEffect to complete measurement and adjustment before paint.

// ❌ Flickers
useEffect(() => {
  setPosition(measureElement());
}, []);

// ✓ No flicker
useLayoutEffect(() => {
  setPosition(measureElement());
}, []);

Summary

  1. Two Phases: Render Phase (pure computation) → Commit Phase (apply + effects)

  2. useEffect: Executes after paint, suits ~99% of side effects

  3. useLayoutEffect: Executes before paint, used for DOM measurement and preventing flicker

  4. useInsertionEffect: Executes before DOM mutation, only for CSS-in-JS libraries

  5. Batching: React 18 automatically batches multiple setState calls

  6. Functional Updates: Use setCount(c => c + 1) to ensure latest state

  7. Performance: Prefer useEffect, use useLayoutEffect only for synchronous operations

Mastering these concepts will significantly enhance your application's stability and performance.


References

Enjoyed this article? Show some love!

0
Clap

Enjoyed this article?

Subscribe for engineering notes and AI development insights

We respect your privacy. No spam, unsubscribe anytime.

Share this article

Comments