- Published on
React Hooks - The Complete Guide with Real-World Examples
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
Introduction
React Hooks transformed how we write React components. No more class components, no more this confusion — just clean, composable functions.
This guide covers every important hook with practical examples you'll use in real projects.
- useState — Local Component State
- useEffect — Side Effects
- useCallback — Memoize Functions
- useMemo — Memoize Expensive Calculations
- useRef — Access DOM and Persist Values
- useContext — Global State Without Props Drilling
- useReducer — Complex State Logic
- Building Custom Hooks
- Conclusion
useState — Local Component State
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>+</button>
<button onClick={() => setCount(c => c - 1)}>-</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
)
}
State with objects:
const [user, setUser] = useState({ name: '', email: '' })
// ✅ Spread to preserve other fields
setUser(prev => ({ ...prev, name: 'Alice' }))
useEffect — Side Effects
import { useState, useEffect } from 'react'
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
let cancelled = false // Prevent state update on unmounted component
async function fetchUser() {
setLoading(true)
try {
const res = await fetch(`/api/users/${userId}`)
const data = await res.json()
if (!cancelled) setUser(data)
} finally {
if (!cancelled) setLoading(false)
}
}
fetchUser()
return () => { cancelled = true } // Cleanup function
}, [userId]) // Re-run when userId changes
if (loading) return <div>Loading...</div>
return <div>{user?.name}</div>
}
useEffect dependency array:
useEffect(() => { /* runs once on mount */ }, [])
useEffect(() => { /* runs on every render */ })
useEffect(() => { /* runs when count changes */ }, [count])
useCallback — Memoize Functions
import { useState, useCallback } from 'react'
function SearchComponent() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
// Without useCallback, this creates a new function on every render
// With useCallback, same function reference unless deps change
const handleSearch = useCallback(async (searchQuery: string) => {
const data = await searchAPI(searchQuery)
setResults(data)
}, []) // No deps — function never changes
return (
<div>
<input
value={query}
onChange={e => {
setQuery(e.target.value)
handleSearch(e.target.value)
}}
/>
{results.map(r => <div key={r.id}>{r.name}</div>)}
</div>
)
}
useMemo — Memoize Expensive Calculations
import { useMemo } from 'react'
function ProductList({ products, filter }) {
// Only recalculates when products or filter changes
const filteredProducts = useMemo(() =>
products
.filter(p => p.category === filter)
.sort((a, b) => a.price - b.price),
[products, filter]
)
return (
<ul>
{filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
)
}
Note: In 2026, the React Compiler handles most memoization automatically. Only use
useMemo/useCallbackfor genuinely expensive operations.
useRef — Access DOM and Persist Values
import { useRef, useEffect } from 'react'
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current?.focus() // Focus on mount
}, [])
return <input ref={inputRef} />
}
// Persist a value without triggering re-render
function Timer() {
const intervalRef = useRef<NodeJS.Timeout | null>(null)
const [count, setCount] = useState(0)
function start() {
intervalRef.current = setInterval(() => setCount(c => c + 1), 1000)
}
function stop() {
if (intervalRef.current) clearInterval(intervalRef.current)
}
return (
<div>
<p>{count}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
)
}
useContext — Global State Without Props Drilling
import { createContext, useContext, useState } from 'react'
// 1. Create context
interface ThemeContextType {
theme: 'light' | 'dark'
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType | null>(null)
// 2. Create provider
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light')
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
// 3. Custom hook for safe usage
function useTheme() {
const context = useContext(ThemeContext)
if (!context) throw new Error('useTheme must be used within ThemeProvider')
return context
}
// 4. Use anywhere in the tree
function Header() {
const { theme, toggleTheme } = useTheme()
return (
<header className={theme}>
<button onClick={toggleTheme}>Toggle {theme}</button>
</header>
)
}
useReducer — Complex State Logic
import { useReducer } from 'react'
type State = { count: number; step: number }
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset' }
| { type: 'setStep'; payload: 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 'reset': return { ...state, count: 0 }
case 'setStep': return { ...state, step: action.payload }
}
}
function AdvancedCounter() {
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 })
return (
<div>
<p>Count: {state.count}</p>
<input
type="number"
value={state.step}
onChange={e => dispatch({ type: 'setStep', payload: +e.target.value })}
/>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
)
}
Building Custom Hooks
Custom hooks let you extract and reuse stateful logic:
// useLocalStorage — persist state in localStorage
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
const setStoredValue = (newValue: T) => {
setValue(newValue)
window.localStorage.setItem(key, JSON.stringify(newValue))
}
return [value, setStoredValue] as const
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light')
return <button onClick={() => setTheme('dark')}>Dark mode</button>
}
// useDebounce — delay state updates
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay)
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}
// Usage
function SearchBox() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 500) // Wait 500ms after typing
useEffect(() => {
if (debouncedQuery) searchAPI(debouncedQuery)
}, [debouncedQuery])
return <input value={query} onChange={e => setQuery(e.target.value)} />
}
Conclusion
Hooks are the heart of modern React. Master useState, useEffect, useRef, and useContext first — they cover 90% of use cases. Then build custom hooks to extract and share logic across components. That's the real superpower: composable, reusable, testable React logic.