13. Hooks Masterclass

Chapter 13: React Hooks Complete Guide

Introduced in React 16.8, React Hooks revolutionized how developers build modern applications by enabling state and lifecycle management inside pure functional components. Learn how hooks operate under the hood via Fiber linked lists, master the core and performance hooks, and build reusable custom hooks.

1. Why Hooks Were Created & The Two Golden Rules

Prior to Hooks, React applications suffered from severe architectural friction:

  • `this` Keyword Confusion: Binding event handlers in constructors (this.handleClick = this.handleClick.bind(this)) caused constant scoping bugs and made code verbosely complicated.
  • Lifecycle Fragmentation: Logic related to a single feature (e.g., setting up a timer and clearing it) had to be split awkwardly across componentDidMount and componentWillUnmount.
  • Wrapper Hell: Sharing stateful logic required complex patterns like Higher-Order Components (HOCs) and Render Props, deeply nesting the component tree in an unreadable hierarchy.
⚠️ The Two Mandatory Rules of Hooks
  1. Only Call Hooks at the Top Level: Do NOT call hooks inside loops, conditional statements (if), or nested functions.
  2. Only Call Hooks from React Functions: Call hooks only from React functional components or custom hooks (functions starting with use).

Why? React stores each component's hooks in a sequential singly-linked list on the Fiber node. React relies on the exact same call order on every render to correctly pair state variables with their values!

2. The Core Hooks: useState, useEffect & useContext

A. `useState` — State Management

Declares a persistent state variable and a dispatch function to update it.

import { useState } from 'react';

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

  return (
    <button onClick={() => setCount(prev => prev + 1)}>
      Clicked {count} times
    </button>
  );
}

B. `useEffect` — Synchronizing Side Effects

Synchronizes your component with an external system (such as browser APIs, network requests, or WebSockets). Always return a cleanup function to prevent memory leaks!

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // 1. Setup an AbortController for clean cancellation
    const controller = new AbortController();

    async function loadUser() {
      try {
        const res = await fetch(`/api/users/${userId}`, { signal: controller.signal });
        const data = await res.json();
        setUser(data);
      } catch (err) {
        if (err.name !== 'AbortError') console.error(err);
      }
    }

    loadUser();

    // 2. Cleanup function: Aborts pending network request if userId changes or component unmounts
    return () => controller.abort();
  }, [userId]); // Dependency array: effect re-runs only when userId changes

  return <div>{user ? user.name : "Loading..."}</div>;
}

C. `useContext` — Teleporting Data Down the Tree

Reads and subscribes to context from your component without tedious prop drilling.

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

const ThemeContext = createContext('light');

function ThemeToggle() {
  const theme = useContext(ThemeContext);
  return <div className={`app-theme-${theme}`}>Current Theme: {theme}</div>;
}

3. Advanced Hooks: useReducer & useRef

useReducer (Complex State Logic)

Ideal when a component has multiple sub-values or when the next state depends on complex actions, resembling the Redux architecture:

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return [...state, action.payload];
    case 'CLEAR':
      return [];
    default:
      return state;
  }
}

// In Component:
const [cart, dispatch] = useReducer(cartReducer, []);
// dispatch({ type: 'ADD_ITEM', payload: product });

useRef (Mutable Refs & DOM Access)

Holds a mutable value across renders without triggering a re-render when mutated. Also provides direct access to real DOM nodes:

function AutoFocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    // Focus the HTML input on mount
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} placeholder="Type here..." />;
}

4. Performance Optimization Hooks: useMemo & useCallback

In JavaScript, functions and objects declared inside a component function are re-created with new memory references on every render. When passing them down to child components wrapped in React.memo, this can cause unnecessary child re-renders.

useMemo: Memoizes Calculated Values

// Only recalculates when 'items' or 'filter' changes
const visibleItems = useMemo(() => {
  return items.filter(item => item.category === filter);
}, [items, filter]);

useCallback: Memoizes Function References

// Retains identical memory reference across renders
const handleDelete = useCallback((id) => {
  setItems(prev => prev.filter(item => item.id !== id));
}, []); // Empty deps: stable forever

5. Authoring Production Custom Hooks

Custom hooks allow you to package stateful logic into reusable, shareable functions. Here is an industry-standard useDebounce hook that delays search queries until the user stops typing:

import { useState, useEffect } from 'react';

// Custom Hook: useDebounce
export function useDebounce(value, delay = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    // Set timer to update value after the delay
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    // Cancel timer if value changes before delay finishes
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// How to use it in any component:
function LiveSearch() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 500);

  useEffect(() => {
    if (debouncedQuery) {
      console.log("Triggering expensive API search for:", debouncedQuery);
    }
  }, [debouncedQuery]);

  return (
    <input 
      type="text" 
      value={query} 
      onChange={(e) => setQuery(e.target.value)} 
      placeholder="Type to search..." 
    />
  );
}

Frequently Asked Questions (FAQ)

Why can't React hooks be called inside conditional statements or loops?

React internally stores a component's hook state as a singly-linked list on the Fiber node (fiber.memoizedState). React does not identify hooks by name; it tracks them solely by their execution order across renders. If you place a hook inside an 'if' statement, the call order between renders shifts, causing pointer misalignment and corrupting your state.

What is the difference between useMemo and useCallback?

useMemo caches the *result* of an expensive calculation (useMemo(() => computeValue(a, b), [a, b])), whereas useCallback caches a *function definition* itself (useCallback(() => handleClick(id), [id])). useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).

When should I use useRef instead of useState?

Use useState when you need to store data that should cause the component to re-render when it changes (like input text or modal open status). Use useRef when you need to store a mutable value that should persist across renders *without* triggering a re-render when modified (like a setInterval ID, a previous prop value, or a direct reference to a DOM node).

What is the useTransition hook in React 18?

useTransition is a concurrent rendering hook that lets you mark certain state updates as non-urgent transitions. While urgent updates (like typing in a search bar) update the screen immediately, transition updates (like filtering a 10,000-row list) execute in the background and can be interrupted by new user keystrokes, keeping the UI silky smooth.

Explore Related Tools

Hand-picked utilities and calculators related to this tool.

Related Tools