Introduction to Hooks
Before React 16.8, any component that needed state or lifecycle behaviour had to be written as a class. Function components were "stateless" — pure UI renderers. Hooks, introduced in React 16.8, changed everything. They let you use state, side effects, context, and more inside ordinary function components.
What Is a Hook?
A hook is a JavaScript function whose name starts with use that lets a function component tap into React features. Hooks do not work inside classes — they are the function-component alternative to class lifecycle methods and instance variables.
// 'useState' is a hook — it hooks your component into React's state system
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0) // ← hook call
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>
}Why Hooks? The Problem They Solve
Class components had three persistent pain points that hooks directly address:
Reusing stateful logic was hard. Patterns like Higher-Order Components and render props worked but wrapped components in layers of nesting (wrapper hell). Custom hooks let you extract and reuse stateful logic without changing the component tree.
Complex components became hard to understand. Related code was split across multiple lifecycle methods (
componentDidMount,componentDidUpdate,componentWillUnmount), and unrelated code was mixed together in each. Hooks let you split code by concern, not by lifecycle method.Classes were confusing. Binding
this, understanding when to use arrow functions vs regular methods, and the subtle differences between lifecycle methods all created friction. Function components with hooks are just functions.
Before Hooks: A Class Component
// React before 16.8 — required a class for stateful behaviour
class WindowWidth extends React.Component {
constructor(props) {
super(props)
this.state = { width: window.innerWidth }
this.handleResize = this.handleResize.bind(this)
}
componentDidMount() {
window.addEventListener('resize', this.handleResize)
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize)
}
handleResize() {
this.setState({ width: window.innerWidth })
}
render() {
return <p>Window width: {this.state.width}px</p>
}
}After Hooks: The Same Logic as a Function Component
import { useState, useEffect } from 'react'
// Identical behaviour, half the code, no class machinery
function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth)
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth)
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize) // cleanup
}, []) // [] means run once on mount
return <p>Window width: {width}px</p>
}And the real power of hooks: you can extract the resize logic into a reusable custom hook and share it across any component:
// hooks/useWindowWidth.js
import { useState, useEffect } from 'react'
export function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth)
useEffect(() => {
function handleResize() { setWidth(window.innerWidth) }
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return width
}
// Now any component can use it in one line:
function Sidebar() {
const width = useWindowWidth()
return width < 768 ? <MobileMenu /> : <DesktopMenu />
}Built-in Hooks at a Glance
Hook | Purpose |
|---|---|
useState | Add local state to a function component |
useEffect | Run side effects (fetch, subscribe, timers) after render |
useContext | Read a Context value without a Consumer wrapper |
useReducer | Manage complex state with a reducer function (like Redux locally) |
useRef | Hold a mutable value or reference a DOM node without triggering re-renders |
useMemo | Cache an expensive computed value between renders |
useCallback | Cache a function reference between renders |
useId | Generate a stable unique ID for accessibility attributes |
useTransition | Mark a state update as non-urgent to keep the UI responsive |
useDeferredValue | Defer re-rendering a slow part of the UI |
useSyncExternalStore | Subscribe to external (non-React) stores safely |
useLayoutEffect | Like useEffect but fires synchronously after DOM mutations |
useDebugValue | Add a label to a custom hook in React DevTools |
Custom Hooks
Any function that calls other hooks and starts with use is a custom hook. Custom hooks are the primary mechanism for sharing stateful logic between components — no class mixins, no HOC wrappers required.
// A custom hook that manages a boolean toggle
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue)
const toggle = () => setValue(v => !v)
return [value, toggle]
}
// Usage
function Accordion({ title, children }) {
const [isOpen, toggleOpen] = useToggle(false)
return (
<div>
<button onClick={toggleOpen}>{isOpen ? '▾' : '▸'} {title}</button>
{isOpen && <div>{children}</div>}
</div>
)
}Hooks Do Not Replace Everything
A few scenarios still require class components:
Error Boundaries — the
componentDidCatchandgetDerivedStateFromErrorlifecycle methods have no hook equivalent. You still write a class component as an error boundary wrapper.Legacy codebases — existing class components do not need to be rewritten. Hooks and class components coexist in the same app.
Some very old lifecycle methods (
getSnapshotBeforeUpdate,UNSAFE_componentWillReceiveProps) also lack hook equivalents — though they are rarely needed.