Hooks are the core of modern React. Here's each one explained with real examples — not contrived counters, but the kinds of patterns you'll actually use in production code.
useState — Local Component State
import { useState } from 'react'
function TemplateCard({ name, price }: { name: string; price: number }) {
const [liked, setLiked] = useState(false)
const [quantity, setQuantity] = useState(1)
const [showDetail, setShowDetail] = useState(false)
const total = price * quantity
return (
<div className="border rounded-2xl p-5 shadow-sm bg-white">
<div className="flex justify-between items-start mb-3">
<h3 className="font-bold text-lg">{name}</h3>
<button
onClick={() => setLiked(l => !l)}
className={`text-xl ${liked ? 'text-red-500' : 'text-gray-300'}`}
>
♥
</button>
</div>
<div className="flex items-center gap-3 mb-4">
<button onClick={() => setQuantity(q => Math.max(1, q - 1))}
className="w-8 h-8 border rounded-lg">−</button>
<span className="font-bold">{quantity}</span>
<button onClick={() => setQuantity(q => q + 1)}
className="w-8 h-8 border rounded-lg">+</button>
</div>
<p className="text-xl font-bold text-red-500 mb-3">${total}</p>
<button
onClick={() => setShowDetail(d => !d)}
className="text-sm text-blue-600 underline"
>
{showDetail ? 'Hide' : 'Show'} details
</button>
{showDetail && (
<p className="text-sm text-gray-500 mt-2">
Angular 21 + Bootstrap 5 admin template with dark mode and RTL.
</p>
)}
</div>
)
}
State updater functions (setQuantity(q => q + 1)) are safer than direct values (setQuantity(quantity + 1)) because they always use the latest state, even in stale closures.
useEffect — Side Effects
import { useState, useEffect } from 'react'
function TemplateList() {
const [templates, setTemplates] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let cancelled = false // prevent state update on unmounted component
async function fetchTemplates() {
try {
const res = await fetch('/api/templates')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
if (!cancelled) {
setTemplates(data)
setLoading(false)
}
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err.message : 'Failed to load')
setLoading(false)
}
}
}
fetchTemplates()
return () => { cancelled = true } // cleanup
}, []) // empty array = run once on mount
if (loading) return <div className="animate-pulse">Loading...</div>
if (error) return <div className="text-red-500">{error}</div>
return (
<div className="grid grid-cols-3 gap-4">
{templates.map(t => <div key={t.id}>{t.name}</div>)}
</div>
)
}
useEffect dependency array:
useEffect(() => { /* runs once on mount */ }, [])
useEffect(() => { /* runs on every render */ })
useEffect(() => { /* runs when userId changes */ }, [userId])
useRef — DOM Access & Mutable Values
import { useRef, useState, useEffect } from 'react'
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const inputRef = useRef<HTMLInputElement>(null)
const timerRef = useRef<ReturnType<typeof setTimeout>>()
const [query, setQuery] = useState('')
// Auto-focus on mount
useEffect(() => {
inputRef.current?.focus()
}, [])
// Debounce search — timerRef doesn't cause re-renders
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value
setQuery(value)
clearTimeout(timerRef.current)
timerRef.current = setTimeout(() => onSearch(value), 400)
}
return (
<div className="relative">
<input
ref={inputRef}
value={query}
onChange={handleChange}
placeholder="Search templates..."
className="w-full border-2 rounded-xl px-4 py-2 focus:border-red-400 outline-none"
/>
{query && (
<button
onClick={() => { setQuery(''); inputRef.current?.focus() }}
className="absolute right-3 top-2.5 text-gray-400"
>
✕
</button>
)}
</div>
)
}
useMemo — Expensive Computations
import { useState, useMemo } from 'react'
interface Template {
id: number
name: string
framework: string
price: number
rating: number
}
function TemplateGrid({ templates }: { templates: Template[] }) {
const [search, setSearch] = useState('')
const [framework, setFramework] = useState('All')
const [sortBy, setSortBy] = useState<'price' | 'rating'>('rating')
// Filtered + sorted list — only recalculates when dependencies change
const filtered = useMemo(() => {
return templates
.filter(t => framework === 'All' || t.framework === framework)
.filter(t => t.name.toLowerCase().includes(search.toLowerCase()))
.sort((a, b) => sortBy === 'price' ? a.price - b.price : b.rating - a.rating)
}, [templates, search, framework, sortBy])
// Summary stats — also memoized
const stats = useMemo(() => ({
total: filtered.length,
avgPrice: filtered.reduce((s, t) => s + t.price, 0) / (filtered.length || 1),
}), [filtered])
return (
<div>
<div className="flex gap-3 mb-4">
<input value={search} onChange={e => setSearch(e.target.value)}
placeholder="Search..." className="border rounded-lg px-3 py-2 flex-1" />
<select value={framework} onChange={e => setFramework(e.target.value)}
className="border rounded-lg px-3 py-2">
<option>All</option>
<option>Angular</option>
<option>React</option>
<option>Bootstrap</option>
</select>
</div>
<p className="text-sm text-gray-500 mb-4">
{stats.total} results · avg ${stats.avgPrice.toFixed(0)}
</p>
<div className="grid grid-cols-3 gap-4">
{filtered.map(t => <div key={t.id} className="border rounded-xl p-4">{t.name}</div>)}
</div>
</div>
)
}
useCallback — Stable Function References
import { useState, useCallback, memo } from 'react'
// Child wrapped in memo — only re-renders if props change
const TemplateRow = memo(function TemplateRow({
template, onDelete, onEdit
}: {
template: { id: number; name: string }
onDelete: (id: number) => void
onEdit: (id: number) => void
}) {
console.log('TemplateRow render', template.name)
return (
<div className="flex justify-between items-center p-3 border-b">
<span>{template.name}</span>
<div className="flex gap-2">
<button onClick={() => onEdit(template.id)}
className="text-sm text-blue-600">Edit</button>
<button onClick={() => onDelete(template.id)}
className="text-sm text-red-500">Delete</button>
</div>
</div>
)
})
function TemplateManager() {
const [templates, setTemplates] = useState([
{ id: 1, name: 'Marvel Dashboard' },
{ id: 2, name: 'Kiosk Angular' },
])
// Without useCallback: new function on every render → memo is bypassed
// With useCallback: same reference as long as [] dependencies don't change
const handleDelete = useCallback((id: number) => {
setTemplates(prev => prev.filter(t => t.id !== id))
}, [])
const handleEdit = useCallback((id: number) => {
console.log('Editing', id)
}, [])
return (
<div className="border rounded-2xl overflow-hidden">
{templates.map(t => (
<TemplateRow key={t.id} template={t}
onDelete={handleDelete} onEdit={handleEdit} />
))}
</div>
)
}
Quick Reference
| Hook | Use when |
|---|---|
useState | Local value that causes re-renders when changed |
useEffect | Fetching data, subscriptions, DOM side effects |
useRef | DOM element access, or mutable value that doesn't cause re-renders |
useMemo | Expensive calculation that depends on specific values |
useCallback | Stable function reference passed to memoized children |
useContext | Reading shared state without prop drilling |
useReducer | Complex state with multiple sub-values or actions |
useId | Generating unique IDs for accessibility labels |
Frequently Asked Questions
Hooks are functions that let you use React state and lifecycle features inside functional components. They were introduced in React 16.8 to replace class components. All hooks start with 'use' — useState, useEffect, useRef, useMemo, etc.
useMemo caches a computed value: const total = useMemo(() => items.reduce(...), [items]). useCallback caches a function reference: const handleClick = useCallback(() => doSomething(id), [id]). Both only recalculate when dependencies change. Use them when the computation is expensive or when passing functions/values to child components that use React.memo.
Only call hooks at the top level of a React function — not inside loops, conditions, or nested functions. Only call hooks from React function components or custom hooks. These rules ensure React can reliably track hook state between renders.
useRef has two uses: accessing DOM elements directly (const inputRef = useRef(null), then <input ref={inputRef}>, then inputRef.current.focus()), and storing mutable values that don't trigger re-renders when changed (like timer IDs, previous values, or cached objects).
Need a Full Bootstrap 5 Admin Dashboard?
Get a complete Angular 21 + Bootstrap 5 dashboard with 50+ components — built by the same team behind BootstrapPlanet.
Browse Templates →Use code FIRST30 for 30% off your first purchase.