Episode 2 — React Frontend Architecture NextJS / 2.7 — Useful Hooks in React

2.7.a — Understanding React Hooks

In one sentence: Hooks are plain JavaScript functions with a special contract — they let function components access React internals (state, effects, context, refs) that were previously locked behind class syntax, fundamentally changing how we think about and compose React logic.

Navigation: ← Overview · Next → Rules of Hooks


Table of Contents

  1. What Are Hooks?
  2. The Problem Hooks Solve
  3. A Brief History: Classes to Hooks
  4. The Hooks Proposal — React 16.8
  5. Hooks vs Class Lifecycle — Mental Model Shift
  6. Anatomy of a Hook Call
  7. How Hooks Work Internally — The Linked List
  8. Built-in Hooks Catalogue
  9. useState — First Look
  10. useEffect — First Look
  11. useContext — First Look
  12. useRef — First Look
  13. Hooks Composition — The Superpower
  14. Common Misconceptions
  15. Hooks in the Broader Ecosystem
  16. Key Takeaways

1. What Are Hooks?

Hooks are functions that start with use and let you use React features inside function components.

import { useState, useEffect } from 'react';

function Counter() {
  // useState is a hook — it "hooks into" React's state system
  const [count, setCount] = useState(0);
  
  // useEffect is a hook — it "hooks into" React's effect system
  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);
  
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

The Key Insight

Before hooks, function components were "stateless." They received props, returned JSX, and that was it. If you needed state, lifecycle methods, or refs, you had to use a class component.

Hooks removed that limitation entirely.

Before Hooks (pre-2019):
┌──────────────────────────────────────────┐
│ Class Components                         │
│ ✓ State                                  │
│ ✓ Lifecycle methods                      │
│ ✓ Refs                                   │
│ ✓ Context                                │
│ ✗ Hard to share logic                    │
│ ✗ Confusing "this" binding               │
│ ✗ Verbose syntax                         │
└──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ Function Components                      │
│ ✗ No state                               │
│ ✗ No lifecycle                           │
│ ✗ No refs                                │
│ ✓ Simple syntax                          │
│ ✓ Easy to understand                     │
│ (basically just render functions)        │
└──────────────────────────────────────────┘

After Hooks (2019+):
┌──────────────────────────────────────────┐
│ Function Components + Hooks              │
│ ✓ State (useState, useReducer)           │
│ ✓ Side effects (useEffect)              │
│ ✓ Refs (useRef)                          │
│ ✓ Context (useContext)                   │
│ ✓ Performance (useMemo, useCallback)     │
│ ✓ Custom hooks (composable logic!)       │
│ ✓ Simple syntax                          │
│ ✓ Easy to share logic                    │
└──────────────────────────────────────────┘

What Makes a Function a "Hook"?

A hook is any function that:

  1. Starts with use (convention enforced by the linter)
  2. Calls other hooks inside its body
  3. Must follow the Rules of Hooks (covered in 2.7.b)
// This IS a hook (starts with "use", calls other hooks)
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  
  return width;
}

// This is NOT a hook (doesn't start with "use")
function getWindowWidth() {
  return window.innerWidth; // regular function
}

// This is NOT a hook (starts with "use" but never calls hooks)
// It works, but the linter will complain — misleading name
function useFormatDate(date) {
  return date.toLocaleDateString(); // should be named "formatDate"
}

2. The Problem Hooks Solve

Hooks weren't invented because classes are "bad." They were invented to solve three specific, painful problems that React developers faced daily.

Problem 1: Logic Reuse Was Painful

Before hooks, the only ways to share stateful logic between components were Higher-Order Components (HOCs) and Render Props. Both created "wrapper hell."

// Before hooks: HOC + Render Props = wrapper hell
// Imagine needing: auth + theme + mouse position + window size

export default 
  withAuth(
    withTheme(
      withRouter(
        withWindowSize(
          ({ auth, theme, router, windowSize }) => {
            // Finally, your actual component...
            // But good luck debugging the DevTools tree:
            // <withAuth>
            //   <withTheme>
            //     <withRouter>
            //       <withWindowSize>
            //         <MyComponent />
          }
        )
      )
    )
  );

With hooks:

// After hooks: flat, readable, composable
function MyComponent() {
  const auth = useAuth();
  const theme = useTheme();
  const router = useRouter();
  const windowSize = useWindowSize();
  
  // All logic is right here, easy to read and debug
}

Problem 2: Complex Components Were Hard to Understand

Class components grouped code by lifecycle method, not by what it does. Related logic was split across multiple methods:

class ChatRoom extends React.Component {
  componentDidMount() {
    this.subscribeToChat(this.props.roomId);  // ← Chat logic
    this.startTimer();                         // ← Timer logic
    document.title = this.props.roomId;        // ← Title logic
  }
  
  componentDidUpdate(prevProps) {
    if (prevProps.roomId !== this.props.roomId) {
      this.unsubscribeFromChat(prevProps.roomId); // ← Chat logic
      this.subscribeToChat(this.props.roomId);     // ← Chat logic
      document.title = this.props.roomId;          // ← Title logic
    }
  }
  
  componentWillUnmount() {
    this.unsubscribeFromChat(this.props.roomId); // ← Chat logic
    this.stopTimer();                             // ← Timer logic
  }
  
  // Chat logic is scattered across THREE methods!
  // Timer logic is scattered across TWO methods!
  // Title logic is scattered across TWO methods!
}

With hooks — code is organized by concern:

function ChatRoom({ roomId }) {
  // ✅ Chat logic — all in one place
  useEffect(() => {
    const connection = subscribeToChat(roomId);
    return () => connection.unsubscribe();
  }, [roomId]);
  
  // ✅ Timer logic — all in one place  
  useEffect(() => {
    const id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, []);
  
  // ✅ Title logic — all in one place
  useEffect(() => {
    document.title = roomId;
  }, [roomId]);
}

Problem 3: Classes Confuse Both People and Machines

Classes introduced several pain points:

ProblemDescription
this bindingForgetting .bind(this) in constructors caused mysterious bugs
Method binding patterns3+ ways to bind: constructor, class field, arrow in render
Optimization barriersClasses don't minify as well, hot reloading is unreliable
Verbose boilerplateconstructor, super(props), this.state, this.setState
Mental overheadUnderstanding prototypal inheritance, this context rules
// Class: how many ways can you bind an event handler?

class Button extends React.Component {
  constructor(props) {
    super(props);
    this.state = { clicked: false };
    this.handleClick = this.handleClick.bind(this); // Way 1
  }
  
  handleClick() { /* ... */ }
  
  // Way 2: class field
  handleClick2 = () => { /* ... */ };
  
  render() {
    return (
      <>
        <button onClick={this.handleClick}>Way 1</button>
        <button onClick={this.handleClick2}>Way 2</button>
        {/* Way 3: arrow in render (creates new function every render!) */}
        <button onClick={() => this.handleClick()}>Way 3</button>
      </>
    );
  }
}
// Function + hooks: there's ONE way
function Button() {
  const [clicked, setClicked] = useState(false);
  
  const handleClick = () => { /* ... */ };
  
  return <button onClick={handleClick}>Click</button>;
}

3. A Brief History: Classes to Hooks

Timeline of React Component Patterns:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

2013 ─── React.createClass()
         │  Autobinding, mixins for code reuse
         │  Simple but "magical"
         │
2015 ─── ES6 Class Components (React 0.13)
         │  No more autobinding, no more mixins
         │  Standard JavaScript, but verbose
         │
2015 ─── Stateless Functional Components
         │  "Dumb" components as plain functions
         │  No state, no lifecycle — render only
         │
2016 ─── Higher-Order Components pattern
         │  Replaced mixins for code reuse
         │  Led to "wrapper hell"
         │
2017 ─── Render Props pattern
         │  Alternative to HOCs
         │  Also led to nesting issues
         │
2018 ─── React 16.8 — HOOKS RELEASED
         │  useState, useEffect, useContext, useRef
         │  Custom hooks for logic reuse
         │  Game changer!
         │
2020 ─── Hooks become default pattern
         │  Most new code written with hooks
         │  Class components maintained but not recommended
         │
2022 ─── React 18 — New hooks
         │  useId, useTransition, useDeferredValue
         │  useSyncExternalStore, useInsertionEffect
         │
2024 ─── React 19 — More hooks + Server Components
         │  use(), useFormStatus, useFormState
         │  useOptimistic, useActionState
         │
2025+ ── React Compiler (auto-memoization)
         Hooks remain the foundation
         Compiler reduces need for manual useMemo/useCallback

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

The Mixins → HOCs → Render Props → Hooks Evolution

Each pattern tried to solve logic reuse. Each improved on the last:

PatternEraProsCons
Mixins2013-2015Simple APIName collisions, implicit dependencies, can't use with classes
HOCs2016-2018Composable, no name collisionWrapper hell, prop collision, indirection
Render Props2017-2019Explicit data flowDeep nesting, verbose, hard to optimize
Hooks2019+Flat, composable, no wrapperRules to follow, closure gotchas
// Evolution: sharing "mouse position" logic

// 1. Mixin (2013) — DEPRECATED
var MouseMixin = {
  getInitialState() { return { x: 0, y: 0 }; },
  componentDidMount() { window.addEventListener('mousemove', this._onMouse); },
  _onMouse(e) { this.setState({ x: e.clientX, y: e.clientY }); }
};
// Usage: mixins: [MouseMixin]

// 2. HOC (2016)
function withMouse(Component) {
  return class extends React.Component {
    state = { x: 0, y: 0 };
    handleMouse = (e) => this.setState({ x: e.clientX, y: e.clientY });
    componentDidMount() { window.addEventListener('mousemove', this.handleMouse); }
    componentWillUnmount() { window.removeEventListener('mousemove', this.handleMouse); }
    render() { return <Component {...this.props} mouse={this.state} />; }
  };
}
// Usage: export default withMouse(MyComponent);

// 3. Render Props (2017)
class Mouse extends React.Component {
  state = { x: 0, y: 0 };
  handleMouse = (e) => this.setState({ x: e.clientX, y: e.clientY });
  componentDidMount() { window.addEventListener('mousemove', this.handleMouse); }
  componentWillUnmount() { window.removeEventListener('mousemove', this.handleMouse); }
  render() { return this.props.render(this.state); }
}
// Usage: <Mouse render={({ x, y }) => <Cursor x={x} y={y} />} />

// 4. Hook (2019) — THE WINNER
function useMouse() {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  useEffect(() => {
    const handle = (e) => setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handle);
    return () => window.removeEventListener('mousemove', handle);
  }, []);
  return pos;
}
// Usage: const { x, y } = useMouse();

4. The Hooks Proposal — React 16.8

On October 25, 2018, Sophie Alpert and Dan Abramov presented hooks at React Conf. The proposal had clear goals:

Design Goals

  1. No breaking changes — Hooks are opt-in. Classes still work.
  2. 100% backward compatible — You can adopt hooks gradually.
  3. No plans to remove classes — They'll work indefinitely.
  4. Flat composition — No nesting, no wrapping.
  5. Statically analyzable — Linters can verify correctness.

What Hooks Replace (and What They Don't)

┌────────────────────────────────┬────────────────────────────────┐
│ Class Feature                  │ Hook Replacement               │
├────────────────────────────────┼────────────────────────────────┤
│ this.state = {}                │ useState()                     │
│ this.setState()                │ setState from useState()       │
│ componentDidMount              │ useEffect(() => {}, [])        │
│ componentDidUpdate             │ useEffect(() => {}, [deps])    │
│ componentWillUnmount           │ useEffect cleanup function     │
│ shouldComponentUpdate          │ React.memo + useMemo           │
│ this.context                   │ useContext()                   │
│ createRef()                    │ useRef()                       │
│ Complex state logic            │ useReducer()                   │
│ getDerivedStateFromError       │ No hook equivalent (yet)       │
│ componentDidCatch              │ No hook equivalent (yet)       │
│ getSnapshotBeforeUpdate        │ No hook equivalent             │
└────────────────────────────────┴────────────────────────────────┘

Note: Error boundaries still require class components in 2025. This is the main reason class components aren't fully obsolete.

Gradual Adoption Strategy

// You can mix classes and hooks in the SAME app

// Old class component — still works fine
class Header extends React.Component {
  render() {
    return <h1>{this.props.title}</h1>;
  }
}

// New function component with hooks
function Sidebar() {
  const [isOpen, setIsOpen] = useState(true);
  return <nav className={isOpen ? 'open' : 'closed'}>...</nav>;
}

// They coexist perfectly
function App() {
  return (
    <>
      <Header title="My App" />
      <Sidebar />
    </>
  );
}

5. Hooks vs Class Lifecycle — Mental Model Shift

This is the most important conceptual shift. Stop thinking in lifecycles. Start thinking in synchronization.

Class Mental Model: "When"

With classes, you think about when things happen:

When the component mounts → do X
When the component updates → do Y  
When the component unmounts → do Z

Hooks Mental Model: "What"

With hooks, you think about what you're synchronizing with:

Synchronize the document title with the current count
Synchronize the chat connection with the current room ID
Synchronize the event listener with the current handler

Visual Comparison

Class Lifecycle (organized by WHEN):
┌─────────────────────────────────────────────────┐
│ componentDidMount                                │
│   → subscribe to chat                           │
│   → start timer                                 │
│   → set document title                          │
│   → fetch initial data                          │
├─────────────────────────────────────────────────┤
│ componentDidUpdate                               │
│   → if roomId changed, resubscribe to chat      │
│   → if userId changed, refetch data             │
│   → update document title                       │
├─────────────────────────────────────────────────┤
│ componentWillUnmount                             │
│   → unsubscribe from chat                       │
│   → stop timer                                  │
│   → cancel pending fetch                        │
└─────────────────────────────────────────────────┘

Hooks (organized by WHAT):
┌─────────────────────────────────┐
│ useEffect: Chat subscription    │
│   sync with: roomId             │
│   setup: subscribe              │
│   cleanup: unsubscribe          │
├─────────────────────────────────┤
│ useEffect: Timer                │
│   sync with: nothing (once)     │
│   setup: setInterval            │
│   cleanup: clearInterval        │
├─────────────────────────────────┤
│ useEffect: Document title       │
│   sync with: roomId             │
│   setup: set title              │
│   cleanup: none needed          │
├─────────────────────────────────┤
│ useEffect: Data fetching        │
│   sync with: userId             │
│   setup: fetch                  │
│   cleanup: abort controller     │
└─────────────────────────────────┘

The "Each Render Has Its Own Everything" Model

In classes, this.state always points to the latest state. In hooks, each render has its own snapshot:

// Class: this.state is ALWAYS the latest
class Timer extends React.Component {
  state = { count: 0 };
  
  handleClick = () => {
    setTimeout(() => {
      // this.state.count is the LATEST value, not the value when clicked
      alert(this.state.count);
    }, 3000);
  };
  
  render() {
    return (
      <div>
        <p>{this.state.count}</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>+</button>
        <button onClick={this.handleClick}>Alert</button>
      </div>
    );
  }
}
// Click +, +, +, then Alert → alerts "3" (latest)
// Click Alert, then +, +, + → STILL alerts "3" (latest)

// Hooks: each render captures its own count
function Timer() {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    setTimeout(() => {
      // count is the value from THIS render, not the latest
      alert(count);
    }, 3000);
  };
  
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={handleClick}>Alert</button>
    </div>
  );
}
// Click +, +, +, then Alert → alerts "3" (value when clicked)
// Click Alert, then +, +, + → alerts "0" (value when clicked!)

This is called the closure model. It's not a bug — it's a feature. Each render is a "snapshot" of your UI at a point in time. This makes behavior more predictable.

If you need the latest value in hooks, use a ref:

function Timer() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);
  countRef.current = count; // always up to date
  
  const handleClick = () => {
    setTimeout(() => {
      alert(countRef.current); // reads latest, not closure value
    }, 3000);
  };
}

6. Anatomy of a Hook Call

Every hook call follows a predictable pattern. Let's dissect it:

function MyComponent() {
  // ┌── Destructured return value
  // │           ┌── The hook function
  // │           │          ┌── Initial value / config
  // ▼           ▼          ▼
  const [state, setState] = useState(initialValue);
  //     │       │
  //     │       └── Updater function (stable reference)
  //     └── Current state value (changes trigger re-render)
  
  // ┌── No return value needed
  // │     ┌── The hook function
  // │     │          ┌── Effect function
  // │     │          │                    ┌── Dependency array
  // ▼     ▼          ▼                    ▼
         useEffect(() => { /* ... */ }, [dep1, dep2]);
  //                  │                    │
  //                  │                    └── Controls when effect re-runs
  //                  └── Runs after render, can return cleanup function
  
  // ┌── The context value
  // │         ┌── The hook function
  // │         │              ┌── The context object
  // ▼         ▼              ▼
  const value = useContext(MyContext);
  //    │
  //    └── Current value from nearest Provider above
  
  // ┌── A mutable ref object
  // │         ┌── The hook function
  // │         │          ┌── Initial .current value
  // ▼         ▼          ▼
  const ref   = useRef(initialValue);
  //    │
  //    └── { current: initialValue } — persists across renders
  //        Changing .current does NOT trigger re-render
}

Hook Return Value Patterns

HookReturnsPattern
useState[value, setter]Array destructuring (rename freely)
useReducer[state, dispatch]Array destructuring
useContextvalueDirect value
useRef{ current: value }Mutable ref object
useMemovalueCached computation
useCallbackfunctionCached function reference
useEffectvoidNo return (side effect)
useLayoutEffectvoidNo return (synchronous side effect)
useIdstringUnique ID for SSR hydration
useTransition[isPending, startTransition]Array destructuring
useDeferredValuevalueDeferred version of input

Why Array Destructuring for useState?

// Array destructuring lets you name freely:
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const [isOpen, setIsOpen] = useState(false);

// If it returned an object, you'd have naming conflicts:
const { value: count, setValue: setCount } = useState(0);     // verbose
const { value: name, setValue: setName } = useState('');       // even worse
const { value: isOpen, setValue: setIsOpen } = useState(false); // painful

7. How Hooks Work Internally — The Linked List

Understanding hooks' internal mechanism explains why the rules exist.

The Linked List Model

React stores hooks as a linked list attached to each component's Fiber node. On every render, React walks through this list in order.

First render of MyComponent:
┌──────────────────────────────────────────────────────┐
│ Fiber Node for <MyComponent />                       │
│                                                      │
│ hooks (linked list):                                 │
│                                                      │
│   Hook 0          Hook 1          Hook 2             │
│ ┌──────────┐    ┌──────────┐    ┌──────────┐        │
│ │ useState  │───▶│ useEffect│───▶│ useRef   │        │
│ │ value: 0  │    │ effect:fn│    │ ref:{c:0}│        │
│ │ queue: [] │    │ deps: [] │    │          │        │
│ └──────────┘    └──────────┘    └──────────┘        │
│                                                      │
└──────────────────────────────────────────────────────┘

What Happens on Each Render

function MyComponent() {
  // Render 1: React CREATES hook at position 0
  // Render 2: React READS  hook at position 0
  // Render 3: React READS  hook at position 0
  const [count, setCount] = useState(0);
  
  // Render 1: React CREATES hook at position 1
  // Render 2: React READS  hook at position 1
  // Render 3: React READS  hook at position 1
  useEffect(() => { /* ... */ }, [count]);
  
  // Render 1: React CREATES hook at position 2
  // Render 2: React READS  hook at position 2
  // Render 3: React READS  hook at position 2
  const ref = useRef(null);
}

React relies on call order to match hooks to their stored state. This is why hooks cannot be inside conditionals:

// ❌ BROKEN: conditional hook changes the order
function Bad({ showExtra }) {
  const [name, setName] = useState('');  // Always position 0 ✓
  
  if (showExtra) {
    useEffect(() => { /* ... */ });      // Sometimes position 1...
  }
  
  const [age, setAge] = useState(0);     // Sometimes position 1, sometimes 2!
  // React can't match this to the right stored state
}

// First render (showExtra = true):
// Position 0: useState('') → name ✓
// Position 1: useEffect    → effect ✓
// Position 2: useState(0)  → age ✓

// Second render (showExtra = false):
// Position 0: useState('') → name ✓
// Position 1: useState(0)  → reads EFFECT data instead of age! 💥

Simplified React Source (Pseudocode)

// React's internal hook mechanism (simplified)
let currentFiber = null;
let hookIndex = 0;

function mountState(initialValue) {
  const hook = {
    state: typeof initialValue === 'function' ? initialValue() : initialValue,
    queue: [],
  };
  currentFiber.hooks.push(hook);
  
  const setState = (action) => {
    hook.queue.push(action);
    scheduleRerender(currentFiber);
  };
  
  return [hook.state, setState];
}

function updateState() {
  const hook = currentFiber.hooks[hookIndex++];
  
  // Process queued updates
  let newState = hook.state;
  for (const action of hook.queue) {
    newState = typeof action === 'function' ? action(newState) : action;
  }
  hook.queue = [];
  hook.state = newState;
  
  return [hook.state, hook.setState];
}

// React calls your component:
function renderComponent(fiber) {
  currentFiber = fiber;
  hookIndex = 0;
  
  // Your component runs, calling hooks in order
  const result = fiber.type(fiber.props);
  
  currentFiber = null;
  return result;
}

8. Built-in Hooks Catalogue

React ships with many hooks. Here's the complete catalogue organized by purpose:

State Hooks — "Store data that changes"

HookPurposeWhen to Use
useStateSimple state (string, number, boolean, object)Default choice for any state
useReducerComplex state with multiple sub-values or transitionsWhen state logic is complex, actions are meaningful

Effect Hooks — "Synchronize with the outside world"

HookPurposeWhen to Use
useEffectSide effects after render (data fetching, subscriptions, DOM changes)Default for any side effect
useLayoutEffectSide effects before browser paintMeasuring DOM, preventing visual flicker
useInsertionEffectInsert styles before layout effectsCSS-in-JS library authors only

Context Hooks — "Read values from the component tree"

HookPurposeWhen to Use
useContextRead and subscribe to contextAccessing theme, auth, locale, etc.

Ref Hooks — "Hold values that don't trigger re-render"

HookPurposeWhen to Use
useRefMutable container that persists across rendersDOM references, timers, previous values
useImperativeHandleCustomize the ref exposed to parent componentsLibrary components with imperative APIs

Performance Hooks — "Skip unnecessary work"

HookPurposeWhen to Use
useMemoCache expensive calculationsComputed values, object/array stability
useCallbackCache function referencesPassing callbacks to memoized children
useTransitionMark state updates as non-urgentHeavy re-renders, keeping UI responsive
useDeferredValueDefer updating part of the UIShowing stale content while computing

Identity Hooks — "Generate unique IDs"

HookPurposeWhen to Use
useIdGenerate unique IDs for accessibilityForm labels, ARIA attributes, SSR-safe IDs

Form Hooks (React 19+) — "Handle form state"

HookPurposeWhen to Use
useFormStatusRead status of parent <form>Disable buttons during submission
useActionStateManage state from form actionsServer actions, progressive enhancement
useOptimisticShow optimistic UI during async operationsLike buttons, add to cart, etc.

Other Hooks

HookPurposeWhen to Use
useRead resources (promises, context)Async data in render, conditional context
useSyncExternalStoreSubscribe to external storesLibraries integrating non-React state
useDebugValueCustomize label in DevToolsCustom hook debugging

Hook Usage Frequency (Real-World)

███████████████████████████████████████████  useState        (95% of components)
██████████████████████████████████           useEffect       (70% of components)
██████████████████████████                   useRef          (50% of components)
████████████████████                         useCallback     (35% of components)
███████████████████                          useMemo         (30% of components)
██████████████                               useContext      (25% of components)
█████████                                    useReducer      (15% of components)
██████                                       useId           (10% of components)
████                                         useLayoutEffect (5% of components)
██                                           useTransition   (3% of components)
█                                            others          (<2%)

9. useState — First Look

The most fundamental hook. Creates a piece of state and a function to update it.

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  //     ↑       ↑                ↑
  //     │       │                └── Initial value
  //     │       └── Setter function (stable reference)
  //     └── Current value
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(prev => prev + 1)}>+1 (updater)</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

Key Characteristics

// 1. Initial value is used ONLY on first render
const [items, setItems] = useState([]); // [] used once, ignored on re-renders

// 2. Lazy initialization — function runs only on first render
const [data, setData] = useState(() => {
  return JSON.parse(localStorage.getItem('data')) || {};
  // This expensive parsing happens ONCE, not every render
});

// 3. Setter with exact value
setCount(5); // Replace state with 5

// 4. Setter with updater function (access previous state)
setCount(prev => prev + 1); // Use when new state depends on previous

// 5. Object state — must spread (no merging like this.setState)
const [user, setUser] = useState({ name: 'Alice', age: 25 });
setUser(prev => ({ ...prev, age: 26 })); // Must spread!
// setUser({ age: 26 }); ← WRONG! Loses 'name'

// 6. Batching — multiple setState calls = one re-render
function handleClick() {
  setCount(c => c + 1);
  setName('Bob');
  setIsOpen(false);
  // Only ONE re-render happens (React 18+ batches automatically)
}

10. useEffect — First Look

Synchronizes your component with an external system.

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    // SETUP: runs after render
    let cancelled = false;
    
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        if (!cancelled) setUser(data);
      });
    
    // CLEANUP: runs before next effect and on unmount
    return () => { cancelled = true; };
  }, [userId]); // DEPENDENCIES: re-run when userId changes
  
  if (!user) return <p>Loading...</p>;
  return <h1>{user.name}</h1>;
}

Three Configurations

// 1. No dependency array — runs after EVERY render
useEffect(() => {
  console.log('I run after every single render');
});

// 2. Empty dependency array — runs ONCE (after first render)
useEffect(() => {
  console.log('I run once, like componentDidMount');
  return () => console.log('I clean up on unmount');
}, []);

// 3. With dependencies — runs when dependencies change
useEffect(() => {
  console.log(`roomId changed to ${roomId}`);
  const connection = connect(roomId);
  return () => connection.disconnect();
}, [roomId]);

The Execution Timeline

Component mounts:
  1. React renders JSX
  2. Browser paints screen  ← user sees content
  3. useEffect runs          ← AFTER paint (non-blocking)

State changes:
  1. React re-renders JSX
  2. Browser paints screen
  3. Previous effect cleanup runs
  4. New effect runs

Component unmounts:
  1. Final effect cleanup runs
  2. Component removed from DOM

11. useContext — First Look

Reads a value from the nearest Context Provider above in the tree.

import { createContext, useContext, useState } from 'react';

// 1. Create context
const ThemeContext = createContext('light');

// 2. Provide value
function App() {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeContext.Provider value={theme}>
      <Header />
      <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
        Toggle
      </button>
    </ThemeContext.Provider>
  );
}

// 3. Consume with useContext
function Header() {
  const theme = useContext(ThemeContext);
  // theme = 'light' or 'dark'
  
  return (
    <header className={theme === 'dark' ? 'bg-gray-900 text-white' : 'bg-white'}>
      <h1>My App</h1>
    </header>
  );
}

Key Points

                    ┌── App ──────────────────┐
                    │ ThemeContext.Provider     │
                    │ value="dark"             │
                    │                          │
                    │  ┌── Layout ──────────┐  │
                    │  │                    │  │
                    │  │  ┌── Header ─────┐ │  │
                    │  │  │ useContext()   │ │  │ ← reads "dark"
                    │  │  │ → "dark"      │ │  │
                    │  │  └───────────────┘ │  │
                    │  │                    │  │
                    │  │  ┌── Sidebar ────┐ │  │
                    │  │  │ useContext()   │ │  │ ← reads "dark"
                    │  │  │ → "dark"      │ │  │
                    │  │  └───────────────┘ │  │
                    │  └────────────────────┘  │
                    └──────────────────────────┘
  • Reads from nearest Provider — skips over any components in between
  • Re-renders on change — when Provider value changes, ALL consumers re-render
  • No Provider = default value — uses the value from createContext(default)

12. useRef — First Look

Creates a mutable container that persists across renders without triggering re-renders.

import { useRef, useEffect } from 'react';

function TextInput() {
  const inputRef = useRef(null);
  //                       ↑ initial value of .current
  
  useEffect(() => {
    inputRef.current.focus(); // Access the actual DOM element
  }, []);
  
  return <input ref={inputRef} placeholder="I auto-focus!" />;
}

Two Primary Use Cases

// USE CASE 1: DOM references
function VideoPlayer() {
  const videoRef = useRef(null);
  
  return (
    <div>
      <video ref={videoRef} src="movie.mp4" />
      <button onClick={() => videoRef.current.play()}>Play</button>
      <button onClick={() => videoRef.current.pause()}>Pause</button>
    </div>
  );
}

// USE CASE 2: Mutable values that don't need re-render
function StopWatch() {
  const [time, setTime] = useState(0);
  const intervalRef = useRef(null);  // store timer ID
  const renderCountRef = useRef(0);  // count renders
  
  renderCountRef.current++;
  
  const start = () => {
    intervalRef.current = setInterval(() => {
      setTime(t => t + 1);
    }, 1000);
  };
  
  const stop = () => {
    clearInterval(intervalRef.current);
  };
  
  return (
    <div>
      <p>Time: {time}s</p>
      <p>Renders: {renderCountRef.current}</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

useRef vs useState

FeatureuseStateuseRef
Triggers re-render on change✅ Yes❌ No
Persists across renders✅ Yes✅ Yes
Can hold any value✅ Yes✅ Yes
Access patternvalue / setValue(newValue)ref.current
Best forUI-visible dataInternal values, DOM refs, timers

Rule of thumb: If the user needs to see the value change, use useState. If you need to remember something without affecting the display, use useRef.


13. Hooks Composition — The Superpower

The real magic of hooks isn't any single hook — it's combining them into custom hooks that encapsulate complete features.

// Custom hook: combines useState + useEffect + useRef
function useLocalStorage(key, initialValue) {
  // Read from localStorage on first render
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });
  
  // Sync to localStorage when value changes
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);
  
  return [value, setValue];
}

// Usage — as clean as useState!
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [fontSize, setFontSize] = useLocalStorage('fontSize', 16);
  
  // theme is read from localStorage on mount
  // every change is automatically persisted
}

Composing Hooks into Larger Hooks

// Hook 1: Track online status
function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);
  
  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  
  return isOnline;
}

// Hook 2: Fetch data
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const controller = new AbortController();
    setLoading(true);
    
    fetch(url, { signal: controller.signal })
      .then(res => res.json())
      .then(data => { setData(data); setLoading(false); })
      .catch(err => { if (!controller.signal.aborted) setError(err); setLoading(false); });
    
    return () => controller.abort();
  }, [url]);
  
  return { data, loading, error };
}

// Hook 3: COMPOSE hooks 1 and 2
function useSmartFetch(url) {
  const isOnline = useOnlineStatus();
  const fetchResult = useFetch(isOnline ? url : null);
  
  return {
    ...fetchResult,
    isOnline,
    isOffline: !isOnline,
  };
}

// Usage: entire feature in one line
function UserList() {
  const { data: users, loading, error, isOffline } = useSmartFetch('/api/users');
  
  if (isOffline) return <OfflineBanner />;
  if (loading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

The Composition Diagram

┌─────────────────────────────────────────────────────┐
│ useSmartFetch(url)                                  │
│                                                     │
│  ┌──────────────────────┐  ┌─────────────────────┐  │
│  │ useOnlineStatus()    │  │ useFetch(url)        │  │
│  │                      │  │                      │  │
│  │  ┌────────────────┐  │  │  ┌────────────────┐  │  │
│  │  │ useState        │  │  │  │ useState ×3    │  │  │
│  │  └────────────────┘  │  │  └────────────────┘  │  │
│  │  ┌────────────────┐  │  │  ┌────────────────┐  │  │
│  │  │ useEffect      │  │  │  │ useEffect      │  │  │
│  │  └────────────────┘  │  │  └────────────────┘  │  │
│  └──────────────────────┘  └─────────────────────┘  │
│                                                     │
│  Returns: { data, loading, error, isOnline }        │
└─────────────────────────────────────────────────────┘

14. Common Misconceptions

Misconception 1: "Hooks replace classes completely"

Reality: Error boundaries still need classes. Also, existing class code doesn't need to be rewritten — hooks are for new code.

Misconception 2: "useEffect is componentDidMount"

Reality: useEffect runs after paint (not synchronously like componentDidMount). It thinks in terms of synchronization, not lifecycle. An empty dependency array mimics the timing, but the mental model is different.

Misconception 3: "useState setter re-renders immediately"

Reality: setState is asynchronous. React batches updates and re-renders once. You can't read the new value on the next line:

const [count, setCount] = useState(0);

function handleClick() {
  setCount(5);
  console.log(count); // Still 0! Not 5!
  // The new value is available on the NEXT render
}

Misconception 4: "Custom hooks share state"

Reality: Each component that calls a custom hook gets its own independent copy of that state:

function useCounter() {
  const [count, setCount] = useState(0);
  return { count, increment: () => setCount(c => c + 1) };
}

function A() {
  const { count } = useCounter(); // count = 0 (A's own copy)
  return <p>A: {count}</p>;
}

function B() {
  const { count } = useCounter(); // count = 0 (B's own copy, separate!)
  return <p>B: {count}</p>;
}
// Incrementing A's counter doesn't affect B's counter

Misconception 5: "useRef is just for DOM elements"

Reality: useRef is a general-purpose mutable container. DOM refs are one use case. Others: storing timers, previous values, any mutable data that shouldn't trigger re-renders.

Misconception 6: "Hooks are magic"

Reality: Hooks are plain functions. The "magic" is React's internal bookkeeping (the linked list). Understanding this removes the mystery and explains the rules.


15. Hooks in the Broader Ecosystem

Hooks influenced far beyond React:

Other Frameworks Inspired by Hooks

FrameworkHook-Like FeatureExample
Vue 3Composition APIref(), computed(), watch(), onMounted()
SvelteRunes (Svelte 5)$state(), $derived(), $effect()
Solid.jsSignalscreateSignal(), createEffect(), createMemo()
PreactHooks (same API)Drop-in compatible with React hooks
AngularSignals (v16+)signal(), computed(), effect()

Library Hooks You'll Encounter

// React Router
const navigate = useNavigate();
const params = useParams();
const location = useLocation();

// TanStack Query
const { data, isLoading } = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
const mutation = useMutation({ mutationFn: createUser });

// React Hook Form
const { register, handleSubmit, formState } = useForm();

// Zustand
const bears = useStore(state => state.bears);

// Framer Motion
const controls = useAnimationControls();

// Next.js
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();

The Pattern: Library → Hook API

Modern React libraries almost always expose their features through hooks. This gives you:

  1. Familiar API — if you know hooks, you can learn any library quickly
  2. Composability — combine library hooks with your own
  3. Type safety — hooks work perfectly with TypeScript inference

16. Key Takeaways

  1. Hooks are functions that let function components access React's internal features (state, effects, context, refs).

  2. Three problems solved: logic reuse (replaced HOCs/render props), code organization (group by concern, not lifecycle), and class complexity (no this, simpler syntax).

  3. Mental model shift: stop thinking "when does this run?" (lifecycle) and start thinking "what am I synchronizing with?" (effects).

  4. Each render has its own everything — state values, event handlers, and effects all capture values from their render (closure model).

  5. Hooks store state as a linked list — React identifies hooks by their call order, which is why hooks can't be inside conditionals.

  6. useState for data the user sees, useRef for data you need to remember silently, useEffect for external synchronization, useContext for tree-wide values.

  7. Custom hooks are the real superpower — compose built-in hooks into reusable features that any component can use with a single function call.

  8. Hooks don't share state between components — each call creates an independent copy.

  9. Hooks influenced the entire ecosystem — Vue, Svelte, Solid, Angular all adopted similar patterns.

  10. Error boundaries still need classes — this is the last remaining reason for class components.


Explain-It Challenge

  1. The Elevator Pitch: Your tech lead asks "Why did React add hooks? Classes worked fine." Give a 30-second explanation covering the three core problems hooks solve.

  2. The Closure Question: A junior developer is confused: "I called setCount(5) but when I log count right after, it's still 0!" Explain why this happens using the "each render has its own snapshot" mental model.

  3. The Analogy: Explain how React's internal hook linked list works to a non-programmer. Use an analogy — perhaps a checklist where items must always be in the same order.


Navigation: ← Overview · Next → Rules of Hooks