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
componentDidMountandcomponentWillUnmount. - 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.
- Only Call Hooks at the Top Level: Do NOT call hooks inside loops, conditional statements (
if), or nested functions. - 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 forever5. 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.
