React Hooks Complete Guide — Every Hook Explained with Real Examples
Advertisement
Introduction
Why This Matters
React Hooks replaced class components as the primary way to write React components. Understanding each hook's purpose, when to use it, and when to avoid it is the core skill that separates junior from senior React developers.
useState — Local Component State
useState stores values that, when changed, trigger a re-render of the component.
'use client'
import { useState } from 'react'
function SearchBar() {
const [query, setQuery] = useState('')
const [isLoading, setIsLoading] = useState(false)
async function handleSearch(e: React.FormEvent) {
e.preventDefault()
setIsLoading(true)
await performSearch(query)
setIsLoading(false)
}
return (
<form onSubmit={handleSearch}>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button disabled={isLoading}>{isLoading ? 'Searching...' : 'Search'}</button>
</form>
)
}Use the functional updater form when the new value depends on the previous value: setCount(prev => prev + 1).
useEffect — Side Effects
useEffect runs code after render. Use it for subscriptions, timers, and external system integrations.
'use client'
import { useEffect, useState } from 'react'
function UserPresence({ userId }: { userId: string }) {
const [isOnline, setIsOnline] = useState(false)
useEffect(() => {
const channel = subscribeToPresence(userId, setIsOnline)
// Cleanup prevents memory leaks on unmount
return () => channel.unsubscribe()
}, [userId]) // Re-run if userId changes
return <span>{isOnline ? 'Online' : 'Offline'}</span>
}useCallback — Stable Function References
useCallback returns a memoized function. Use it when passing callbacks to memoized child components to prevent unnecessary re-renders.
'use client'
import { useState, useCallback } from 'react'
import { ItemRow } from './item-row'
function ItemList({ items }: { items: Item[] }) {
const [selected, setSelected] = useState<string | null>(null)
// Without useCallback, a new function is created on every render
const handleSelect = useCallback((id: string) => {
setSelected(id)
}, []) // No dependencies — function never changes
return (
<ul>
{items.map(item => (
<ItemRow key={item.id} item={item} onSelect={handleSelect} isSelected={selected === item.id} />
))}
</ul>
)
}useMemo — Expensive Computations
useMemo caches the result of an expensive calculation and recomputes only when dependencies change.
'use client'
import { useMemo } from 'react'
function DataTable({ data, filter }: { data: Row[]; filter: string }) {
const filteredData = useMemo(() => {
return data
.filter(row => row.name.toLowerCase().includes(filter.toLowerCase()))
.sort((a, b) => a.name.localeCompare(b.name))
}, [data, filter])
return (
<table>
{filteredData.map(row => (
<tr key={row.id}><td>{row.name}</td><td>{row.value}</td></tr>
))}
</table>
)
}useRef — DOM Access and Mutable Values
useRef has two uses: accessing DOM elements directly, and storing mutable values that do not trigger re-renders.
'use client'
import { useRef, useEffect } from 'react'
function VideoPlayer({ src }: { src: string }) {
const videoRef = useRef<HTMLVideoElement>(null)
const timerRef = useRef<NodeJS.Timeout | null>(null)
useEffect(() => {
// Auto-play after 2 seconds
timerRef.current = setTimeout(() => {
videoRef.current?.play()
}, 2000)
return () => {
if (timerRef.current) clearTimeout(timerRef.current)
}
}, [])
return <video ref={videoRef} src={src} controls />
}useContext — Consuming Context
'use client'
import { createContext, useContext, useState } from 'react'
const ThemeContext = createContext<{ theme: string; toggle: () => void } | null>(null)
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
return (
<ThemeContext.Provider value={{ theme, toggle: () => setTheme(t => t === 'light' ? 'dark' : 'light') }}>
{children}
</ThemeContext.Provider>
)
}
export function useTheme() {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme must be used inside ThemeProvider')
return ctx
}useReducer — Complex State Logic
Use useReducer when state transitions are complex or when next state depends on multiple previous values.
'use client'
import { useReducer } from 'react'
type State = { count: number; step: number }
type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'setStep'; step: number }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment': return { ...state, count: state.count + state.step }
case 'decrement': return { ...state, count: state.count - state.step }
case 'setStep': return { ...state, step: action.step }
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 })
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+{state.step}</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-{state.step}</button>
</div>
)
}useTransition and useDeferredValue — Concurrent Features
These hooks keep the UI responsive during expensive updates.
'use client'
import { useState, useTransition, useDeferredValue } from 'react'
function SearchPage() {
const [query, setQuery] = useState('')
const [isPending, startTransition] = useTransition()
const deferredQuery = useDeferredValue(query)
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value)
// Heavy result update is non-urgent
startTransition(() => {
// expensive state updates go here
})
}
return (
<>
<input onChange={handleChange} placeholder="Search..." />
{isPending && <span>Updating...</span>}
{/* deferredQuery lags behind query — keeps input snappy */}
<SearchResults query={deferredQuery} />
</>
)
}Common Mistakes
- Forgetting the dependency array in
useEffect, causing infinite re-render loops - Using
useCallbackanduseMemoeverywhere — they add overhead; only apply to proven bottlenecks - Mutating ref values and expecting a re-render —
useRefchanges do not trigger renders - Missing cleanup in
useEffectsubscriptions and timers, causing memory leaks - Calling hooks conditionally or inside loops — hooks must be called at the top level every render
Best Practices
- Extract related state and logic into custom hooks for reusability and testability
- Use the React DevTools Profiler to identify actual re-render problems before reaching for
useMemo - Prefer
useReducerover multipleuseStatecalls when state values change together - Always specify all reactive dependencies in effect and callback dependency arrays — use the
eslint-plugin-react-hooksrule
Key Takeaways
useStateanduseEffectcover 80% of real-world hook usage; master them firstuseCallbackanduseMemoare performance tools, not correctness tools — measure before applyinguseRefpersists values across renders without triggering a re-render, and provides direct DOM accessuseReducerscales better than multipleuseStatecalls for complex, interdependent stateuseTransitionmarks state updates as non-urgent, keeping input and animations smooth during heavy workuseDeferredValuedelays rendering a value — similar to debouncing but integrated with React's scheduler- Always return cleanup functions from
useEffectwhen working with subscriptions, timers, or event listeners - Custom hooks starting with
usecan call other hooks and encapsulate reusable stateful logic
Advertisement