Skip to content
GeeksSmith
intermediate 9 min read

React Hooks Internals, Lifecycle, and Dependency Traps

React Hooks store stateful logic and side-effects on Fiber nodes as a singly-linked list of hook objects. Their execution order must remain strictly identical on every render.

Why it matters in interviews

Violating the rules of hooks, misusing useEffect dependencies, causing stale closures, and confusing useEffect with useLayoutEffect account for the vast majority of subtle frontend bugs and interview questions.

Visual & Interactive Explanation

Hook Execution Timeline (useLayoutEffect vs useEffect)

Interactive Comparison

useInsertionEffect

Runs before DOM mutations

CSS-in-JS Only
TimingBefore DOM mutations
DOM AccessNo DOM access yet
Use CaseInjecting critical <style> tags
Strengths
  • Prevents style recalculation layout thrashing
Trade-offs
  • Should never be used in regular product application code

useLayoutEffect

Runs before browser paint

Flicker-Free
TimingAfter DOM mutation, BEFORE Paint
BlockingBlocks browser paint synchronously
Use CaseDOM measurements, tooltip positioning
Strengths
  • Zero visual flicker for layout calculations
Trade-offs
  • Slow execution blocks frame budget (causes dropped frames)

useEffect

Runs after browser paint

Default (Non-blocking)
TimingAFTER Paint (Async passive effect)
BlockingNon-blocking (smooth 60fps)
Use CaseData fetching, subscriptions, timers
Strengths
  • Smooth UI responsiveness without blocking user inputs
Trade-offs
  • DOM measurements here may cause visible 1-frame layout jumps

Code Examples & Implementation

Production Custom Hook with AbortController and Cleanup
import { useState, useEffect } from 'react';

export function useFetchData<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    // AbortController cancels fetch if url changes or component unmounts
    const controller = new AbortController();
    setLoading(true);

    fetch(url, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then((json) => {
        setData(json);
        setError(null);
      })
      .catch((err) => {
        if (err.name !== 'AbortError') {
          setError(err);
        }
      })
      .finally(() => setLoading(false));

    // Cleanup: cancel in-flight request on re-render / unmount
    return () => {
      controller.abort();
    };
  }, [url]);

  return { data, loading, error };
}

Interview-Ready Answers

Hooks rely on a linked list attached to the component's Fiber node. On each render, React advances an internal hook pointer along this list, which is why hook order must never change.

Official Documentation & Specifications

Follow-up Questions & Deep Dives

🎯 Code Output — What Does This Print?

Question 1medium
function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
    console.log("count in handler:", count);
  };

  console.log("render:", count);
  return <button onClick={handleClick}>+</button>;
}
// User clicks once. What logs?
Question 2hard
function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount((prev) => prev + 1);
    setCount((prev) => prev + 1);
    setCount((prev) => prev + 1);
  };

  console.log("render:", count);
  return <button onClick={handleClick}>+</button>;
}
// User clicks once. What logs?
Question 3tricky
function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("effect:", count);
    return () => console.log("cleanup:", count);
  }, [count]);

  console.log("render:", count);
  return <button onClick={() => setCount(1)}>Click</button>;
}
// Component mounts, then user clicks once.
Question 4hard
function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <div>{count}</div>;
}
// After 5 seconds, what does count display?

Common Mistakes & Anti-Patterns

  • Using useEffect to sync state from props instead of deriving state during render or using a key to reset state.
  • Omission of cleanup functions for subscriptions, leading to memory leaks and setState on unmounted component warnings.
  • Using useLayoutEffect on the server during SSR (triggers React warning because SSR has no DOM layout phase).

Real-World Architectural Scenario

A tooltip component briefly flickers in the top-left corner before snapping to the correct target button position. How do you fix it?

Rate Your Readiness

Rate how comfortably you can explain this in an interview to update your global readiness gauge:

Rate your confidence: