Crash Courses · Masters
Custom Hooks
Extract and reuse stateful logic 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
Custom hooks extract stateful logic into reusable functions. They follow the "use" naming convention and can call other hooks.
useLocalStorage
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 full */ }
}, [key, value])
return [value, setValue] as const
}
// Usage
function App() {
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light')
}useFetch
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) })
.catch(e => { if (!cancelled) setError(e.message) })
.finally(() => { if (!cancelled) setLoading(false) })
return () => { cancelled = true }
}, [url])
return { data, loading, error }
}
// Usage
function CourseDetail({ id }: { id: string }) {
const { data: course, loading, error } = useFetch<Course>(`/api/courses/${id}`)
if (loading) return <Spinner />
if (error) return <Error message={error} />
return <CourseView course={course!} />
}useDebounce
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay)
return () => clearTimeout(timer)
}, [value, delay])
return debounced
}
// Usage — search input that waits 300ms before searching
function SearchInput() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
useEffect(() => {
if (debouncedQuery) search(debouncedQuery)
}, [debouncedQuery])
return <input value={query} onChange={e => setQuery(e.target.value)} />
}Reading progress
0% read
In this module
Custom Hook
Hook composition
Separation of concerns
useFetch pattern
0%