Custom hooks are one of React's most powerful patterns — they let you extract any stateful logic into a reusable function. Here are 7 production-ready custom hooks you can drop into any project.
useDebounce
Delays updating a value until the user stops typing. Essential for search inputs — prevents firing an API call on every keystroke:
import { useState, useEffect } from 'react'
function useDebounce<T>(value: T, delay: number = 400): T {
const [debounced, setDebounced] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay)
return () => clearTimeout(timer)
}, [value, delay])
return debounced
}
// Usage
function TemplateSearch() {
const [search, setSearch] = useState('')
const [results, setResults] = useState<any[]>([])
const debouncedSearch = useDebounce(search, 400)
useEffect(() => {
if (!debouncedSearch) { setResults([]); return }
fetch(`/api/templates?q=${debouncedSearch}`)
.then(r => r.json())
.then(setResults)
}, [debouncedSearch]) // only fires 400ms after user stops typing
return (
<div>
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search templates..."
className="border-2 rounded-xl px-4 py-2 w-full focus:border-red-400 outline-none"
/>
<p className="text-sm text-gray-500 mt-1">{results.length} results</p>
</div>
)
}
useLocalStorage
Syncs state with localStorage so it persists across page refreshes:
import { useState, useEffect } from 'react'
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value))
} catch (e) {
console.warn('localStorage write failed:', e)
}
}, [key, value])
return [value, setValue] as const
}
// Usage
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light')
useEffect(() => {
document.documentElement.setAttribute('data-bs-theme', theme)
}, [theme])
return (
<button
onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}
className={`btn btn-sm ${theme === 'dark' ? 'btn-light' : 'btn-dark'}`}
>
{theme === 'dark' ? '☀️ Light' : '🌙 Dark'}
</button>
)
}
useFetch
Generic data-fetching hook with loading and error states:
import { useState, useEffect, useRef } from 'react'
interface FetchState<T> {
data: T | null
loading: boolean
error: string | null
refetch: () => void
}
function useFetch<T>(url: string): FetchState<T> {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const counter = useRef(0)
function fetch_() {
const id = ++counter.current
setLoading(true)
setError(null)
fetch(url)
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json() })
.then(d => { if (counter.current === id) { setData(d); setLoading(false) } })
.catch(e => { if (counter.current === id) { setError(e.message); setLoading(false) } })
}
useEffect(() => { fetch_() }, [url])
return { data, loading, error, refetch: fetch_ }
}
// Usage
function TemplateDetail({ id }: { id: number }) {
const { data: template, loading, error, refetch } = useFetch<{
name: string; framework: string; price: number
}>(`/api/templates/${id}`)
if (loading) return <div className="spinner-border" style={{ color: '#fd4766' }} />
if (error) return (
<div className="alert alert-danger d-flex align-items-center gap-2">
{error}
<button className="btn btn-sm btn-outline-danger ms-auto" onClick={refetch}>Retry</button>
</div>
)
return (
<div className="card border-0 shadow-sm p-4">
<h2 className="fw-bold">{template!.name}</h2>
<span className="badge" style={{ background: '#fd4766' }}>{template!.framework}</span>
<p className="fw-bold mt-2" style={{ color: '#fd4766' }}>${template!.price}</p>
</div>
)
}
useMediaQuery
Tracks a CSS media query — useful for showing different UI at different breakpoints:
import { useState, useEffect } from 'react'
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(
() => window.matchMedia(query).matches
)
useEffect(() => {
const mq = window.matchMedia(query)
const handler = (e: MediaQueryListEvent) => setMatches(e.matches)
mq.addEventListener('change', handler)
return () => mq.removeEventListener('change', handler)
}, [query])
return matches
}
// Usage
function Navbar() {
const isMobile = useMediaQuery('(max-width: 767px)')
const isDark = useMediaQuery('(prefers-color-scheme: dark)')
return (
<nav className="navbar navbar-expand-lg px-4"
style={{ background: isDark ? '#0d0d0d' : '#fff' }}>
<a className="navbar-brand fw-bold" href="/">BootstrapPlanet</a>
{!isMobile && (
<div className="navbar-nav ms-auto gap-3">
<a className="nav-link" href="/components">Components</a>
<a className="nav-link" href="/angular">Angular</a>
</div>
)}
{isMobile && (
<button className="navbar-toggler">
<span className="navbar-toggler-icon" />
</button>
)}
</nav>
)
}
useOnClickOutside
Detects clicks outside an element — for dropdowns, modals, and off-canvas panels:
import { useEffect, RefObject } from 'react'
function useOnClickOutside(
ref: RefObject<HTMLElement | null>,
handler: (event: MouseEvent | TouchEvent) => void
) {
useEffect(() => {
function listener(event: MouseEvent | TouchEvent) {
if (!ref.current || ref.current.contains(event.target as Node)) return
handler(event)
}
document.addEventListener('mousedown', listener)
document.addEventListener('touchstart', listener)
return () => {
document.removeEventListener('mousedown', listener)
document.removeEventListener('touchstart', listener)
}
}, [ref, handler])
}
// Usage
function Dropdown() {
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
useOnClickOutside(ref, () => setOpen(false))
return (
<div ref={ref} className="position-relative d-inline-block">
<button
className="btn text-white"
style={{ background: '#fd4766' }}
onClick={() => setOpen(o => !o)}
>
Templates ▾
</button>
{open && (
<div className="dropdown-menu show shadow border-0 mt-1">
<a className="dropdown-item" href="/angular">Angular 21</a>
<a className="dropdown-item" href="/react">React 19</a>
<a className="dropdown-item" href="/components">Bootstrap 5</a>
</div>
)}
</div>
)
}
useCopyToClipboard
Handles copy-to-clipboard with a "Copied!" feedback state:
import { useState, useCallback } from 'react'
function useCopyToClipboard(resetDelay = 2000) {
const [copied, setCopied] = useState(false)
const copy = useCallback(async (text: string) => {
try {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), resetDelay)
} catch {
// Fallback for older browsers
const el = document.createElement('textarea')
el.value = text
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
setCopied(true)
setTimeout(() => setCopied(false), resetDelay)
}
}, [resetDelay])
return { copied, copy }
}
// Usage
function CodeBlock({ code }: { code: string }) {
const { copied, copy } = useCopyToClipboard()
return (
<div className="position-relative">
<pre className="bg-dark text-white rounded-3 p-4 mb-0"
style={{ fontSize: '0.85rem' }}>{code}</pre>
<button
onClick={() => copy(code)}
className="btn btn-sm position-absolute top-0 end-0 m-2"
style={{
background: copied ? '#10b981' : '#fd4766',
color: '#fff',
border: 'none',
transition: 'background 0.2s'
}}
>
{copied ? '✓ Copied!' : 'Copy'}
</button>
</div>
)
}
Naming and Rules
Every custom hook must start with use — this isn't just convention, it's how React's linting rules identify hooks and enforce the rules-of-hooks. If your function doesn't start with use, calling useState inside it will either fail silently or throw.
Structure your hooks file by scope:
src/hooks/
├── useDebounce.ts
├── useLocalStorage.ts
├── useFetch.ts
├── useMediaQuery.ts
└── useOnClickOutside.ts
Or co-locate a hook next to the component that owns it if it's not shared. The goal is reusable, testable logic — not necessarily a single global hooks folder.
Frequently Asked Questions
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.