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
- What Are Hooks?
- The Problem Hooks Solve
- A Brief History: Classes to Hooks
- The Hooks Proposal — React 16.8
- Hooks vs Class Lifecycle — Mental Model Shift
- Anatomy of a Hook Call
- How Hooks Work Internally — The Linked List
- Built-in Hooks Catalogue
- useState — First Look
- useEffect — First Look
- useContext — First Look
- useRef — First Look
- Hooks Composition — The Superpower
- Common Misconceptions
- Hooks in the Broader Ecosystem
- 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:
- Starts with
use(convention enforced by the linter) - Calls other hooks inside its body
- 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:
| Problem | Description |
|---|---|
this binding | Forgetting .bind(this) in constructors caused mysterious bugs |
| Method binding patterns | 3+ ways to bind: constructor, class field, arrow in render |
| Optimization barriers | Classes don't minify as well, hot reloading is unreliable |
| Verbose boilerplate | constructor, super(props), this.state, this.setState |
| Mental overhead | Understanding 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:
| Pattern | Era | Pros | Cons |
|---|---|---|---|
| Mixins | 2013-2015 | Simple API | Name collisions, implicit dependencies, can't use with classes |
| HOCs | 2016-2018 | Composable, no name collision | Wrapper hell, prop collision, indirection |
| Render Props | 2017-2019 | Explicit data flow | Deep nesting, verbose, hard to optimize |
| Hooks | 2019+ | Flat, composable, no wrapper | Rules 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
- No breaking changes — Hooks are opt-in. Classes still work.
- 100% backward compatible — You can adopt hooks gradually.
- No plans to remove classes — They'll work indefinitely.
- Flat composition — No nesting, no wrapping.
- 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
| Hook | Returns | Pattern |
|---|---|---|
useState | [value, setter] | Array destructuring (rename freely) |
useReducer | [state, dispatch] | Array destructuring |
useContext | value | Direct value |
useRef | { current: value } | Mutable ref object |
useMemo | value | Cached computation |
useCallback | function | Cached function reference |
useEffect | void | No return (side effect) |
useLayoutEffect | void | No return (synchronous side effect) |
useId | string | Unique ID for SSR hydration |
useTransition | [isPending, startTransition] | Array destructuring |
useDeferredValue | value | Deferred 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"
| Hook | Purpose | When to Use |
|---|---|---|
useState | Simple state (string, number, boolean, object) | Default choice for any state |
useReducer | Complex state with multiple sub-values or transitions | When state logic is complex, actions are meaningful |
Effect Hooks — "Synchronize with the outside world"
| Hook | Purpose | When to Use |
|---|---|---|
useEffect | Side effects after render (data fetching, subscriptions, DOM changes) | Default for any side effect |
useLayoutEffect | Side effects before browser paint | Measuring DOM, preventing visual flicker |
useInsertionEffect | Insert styles before layout effects | CSS-in-JS library authors only |
Context Hooks — "Read values from the component tree"
| Hook | Purpose | When to Use |
|---|---|---|
useContext | Read and subscribe to context | Accessing theme, auth, locale, etc. |
Ref Hooks — "Hold values that don't trigger re-render"
| Hook | Purpose | When to Use |
|---|---|---|
useRef | Mutable container that persists across renders | DOM references, timers, previous values |
useImperativeHandle | Customize the ref exposed to parent components | Library components with imperative APIs |
Performance Hooks — "Skip unnecessary work"
| Hook | Purpose | When to Use |
|---|---|---|
useMemo | Cache expensive calculations | Computed values, object/array stability |
useCallback | Cache function references | Passing callbacks to memoized children |
useTransition | Mark state updates as non-urgent | Heavy re-renders, keeping UI responsive |
useDeferredValue | Defer updating part of the UI | Showing stale content while computing |
Identity Hooks — "Generate unique IDs"
| Hook | Purpose | When to Use |
|---|---|---|
useId | Generate unique IDs for accessibility | Form labels, ARIA attributes, SSR-safe IDs |
Form Hooks (React 19+) — "Handle form state"
| Hook | Purpose | When to Use |
|---|---|---|
useFormStatus | Read status of parent <form> | Disable buttons during submission |
useActionState | Manage state from form actions | Server actions, progressive enhancement |
useOptimistic | Show optimistic UI during async operations | Like buttons, add to cart, etc. |
Other Hooks
| Hook | Purpose | When to Use |
|---|---|---|
use | Read resources (promises, context) | Async data in render, conditional context |
useSyncExternalStore | Subscribe to external stores | Libraries integrating non-React state |
useDebugValue | Customize label in DevTools | Custom 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
| Feature | useState | useRef |
|---|---|---|
| Triggers re-render on change | ✅ Yes | ❌ No |
| Persists across renders | ✅ Yes | ✅ Yes |
| Can hold any value | ✅ Yes | ✅ Yes |
| Access pattern | value / setValue(newValue) | ref.current |
| Best for | UI-visible data | Internal 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, useuseRef.
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
| Framework | Hook-Like Feature | Example |
|---|---|---|
| Vue 3 | Composition API | ref(), computed(), watch(), onMounted() |
| Svelte | Runes (Svelte 5) | $state(), $derived(), $effect() |
| Solid.js | Signals | createSignal(), createEffect(), createMemo() |
| Preact | Hooks (same API) | Drop-in compatible with React hooks |
| Angular | Signals (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:
- Familiar API — if you know hooks, you can learn any library quickly
- Composability — combine library hooks with your own
- Type safety — hooks work perfectly with TypeScript inference
16. Key Takeaways
-
Hooks are functions that let function components access React's internal features (state, effects, context, refs).
-
Three problems solved: logic reuse (replaced HOCs/render props), code organization (group by concern, not lifecycle), and class complexity (no
this, simpler syntax). -
Mental model shift: stop thinking "when does this run?" (lifecycle) and start thinking "what am I synchronizing with?" (effects).
-
Each render has its own everything — state values, event handlers, and effects all capture values from their render (closure model).
-
Hooks store state as a linked list — React identifies hooks by their call order, which is why hooks can't be inside conditionals.
-
useState for data the user sees, useRef for data you need to remember silently, useEffect for external synchronization, useContext for tree-wide values.
-
Custom hooks are the real superpower — compose built-in hooks into reusable features that any component can use with a single function call.
-
Hooks don't share state between components — each call creates an independent copy.
-
Hooks influenced the entire ecosystem — Vue, Svelte, Solid, Angular all adopted similar patterns.
-
Error boundaries still need classes — this is the last remaining reason for class components.
Explain-It Challenge
-
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.
-
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.
-
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