Crash Courses · Masters
Custom Hooks
Extract and reuse stateful logic across components with custom hooks.
10 min read+175 XP on completionCert: React Crash Course
Tap any word in the text below to start reading from there.
Custom Hooks
When multiple components need the same stateful logic — data fetching, localStorage sync, debouncing — the right solution is a custom hook. Extract the logic once, reuse it everywhere.
The Pattern
A custom hook is just a JavaScript function that:
- Starts with
use(so React can enforce the rules of hooks) - Calls other hooks (useState, useEffect, built-in or custom)
- Returns values the component needs
useLocalStorage — Persisted State
import { useState, useEffect } from 'react'
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const stored = localStorage.getItem(key)
return stored ? JSON.parse(stored) : initialValue
} catch {
return initialValue
}
})
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value))
} catch { /* storage quota exceeded */ }
}, [key, value])
return [value, setValue] as const
}
// Usage — drop-in replacement for useState with persistence
function App() {
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light')
const [sidebarOpen, setSidebarOpen] = useLocalStorage('sidebar', true)
}useFetch — Data Fetching with Loading State
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let cancelled = false
fetch(url)
.then(r => r.json())
.then(d => { if (!cancelled) { setData(d); setError(null) } })
.catch(e => { if (!cancelled) setError(e.message) })
.finally(() => { if (!cancelled) setLoading(false) })
return () => { cancelled = true }
}, [url])
return { data, loading, error }
}
// Usage — components get clean data with no loading boilerplate
function CourseDetail({ id }: { id: string }) {
const { data: course, loading, error } = useFetch<Course>(`/api/courses/${id}`)
if (loading) return <Spinner />
if (error) return <ErrorMessage message={error} />
return <CourseView course={course!} />
}useDebounce — Delay Expensive Operations
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay)
return () => clearTimeout(timer) // cancel the previous timer on each change
}, [value, delay])
return debounced
}
// Usage — search fires only after 300ms of inactivity
function CourseSearch() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
useEffect(() => {
if (debouncedQuery) searchCourses(debouncedQuery)
}, [debouncedQuery])
return (
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search courses..."
/>
)
}useMediaQuery — Responsive Logic in JS
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(
() => window.matchMedia(query).matches
)
useEffect(() => {
const mql = window.matchMedia(query)
const handler = (e: MediaQueryListEvent) => setMatches(e.matches)
mql.addEventListener('change', handler)
return () => mql.removeEventListener('change', handler)
}, [query])
return matches
}
// Usage
function NavBar() {
const isMobile = useMediaQuery('(max-width: 768px)')
return isMobile ? <MobileNav /> : <DesktopNav />
}The key insight: hooks compose. useDebounce uses useState and useEffect. useFetch uses useState, useEffect, and could use useDebounce. You build complex, reusable behavior by composing simple primitives.
Reading progress
0% read
In this module
Custom Hook
Hook composition
Separation of concerns
useFetch pattern
0%