Lesson 5 of 12
Custom Hooks
You will learn how to write custom hooks: that start with `use` and call other hooks. They let you reuse the same stateful logic in several components.
import { useState, useEffect } from 'react'
// useFetch — reusable data fetching
function useFetch(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false
setLoading(true)
fetch(url)
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json() })
.then(d => { if (!cancelled) setData(d) })
.catch(e => { if (!cancelled) setError(e) })
.finally(() => { if (!cancelled) setLoading(false) })
return () => { cancelled = true }
}, [url])
return { data, loading, error }
}
// Usage — clean and readable
function PostList() {
const { data: posts, loading, error } = useFetch('/api/posts')
if (loading) return <p>Loading...</p>
if (error) return <p>Error: {error.message}</p>
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}import { useState, useCallback, useEffect } from 'react'
// useLocalStorage — sync state with localStorage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
const set = useCallback(val => {
setValue(val)
window.localStorage.setItem(key, JSON.stringify(val))
}, [key])
return [value, set]
}
// useDebounce — delay updating a value
function useDebounce(value, delay = 300) {
const [debounced, setDebounced] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay)
return () => clearTimeout(timer)
}, [value, delay])
return debounced
}
// useDebounce in practice — search that doesn't fire on every keystroke
function SearchBox() {
const [query, setQuery] = useState("")
const debouncedQuery = useDebounce(query, 400)
const { data: results } = useFetch(
debouncedQuery ? `/api/search?q=${encodeURIComponent(debouncedQuery)}` : null
)
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search..." />
{results?.map(r => <div key={r.id}>{r.title}</div>)}
</div>
)
}Finished reading?Mark it done to track your progress.
Tip: use the ← and → keys to move between lessons.