JavaScript Curriculum
Custom Hooks
mediumThree different menu components all fetch data, manage loading state, and handle errors in the same way. Instead of repeating the pattern, extract it into a useFetch hook — one place to maintain, used everywhere.
Custom Hooks
A custom hook is a function that starts with use and calls other hooks. That is the entire definition. It extracts stateful logic from a component so the same logic can be shared between multiple components without duplicating code.
Four Custom Hook Patterns
useLocalStorage, useDebounce, useOnline, usePrevious:
Delays updating a value until the user stops changing it. Prevents firing search API on every keystroke.
function useDebounce(value, delay = 300) { const [debounced, setDebounced] = useState(value) useEffect(() => { const timer = setTimeout(() => { setDebounced(value) }, delay) return () => clearTimeout(timer) // clear on next change }, [value, delay]) return debounced }
use can call other hooks. Plain functions cannot.The Three Hook Rules
Why these rules exist and what breaks when you violate them:
Only call hooks at the top level of your component or custom hook — not inside loops, conditions, or nested functions. React relies on the order of hook calls to associate state with the right hook.
// ❌ Conditional hook — BREAKS React's call order function Component({ show }) { if (show) { const [data, setData] = useState(null) // conditional! } const [name, setName] = useState('') // order shifts! }
// ✓ Hooks always at top level — consistent order function Component({ show }) { const [data, setData] = useState(null) // always called const [name, setName] = useState('') // always called // Move the condition INSIDE the component logic: if (!show) return null }
useFetch — A Production-Ready Example
The most common custom hook — data fetching with loading, error, and abort:
// The custom hook — reusable fetch logic function useFetch(url) { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { const controller = new AbortController() setLoading(true) setError(null) fetch(url, { signal: controller.signal }) .then(res => { if (!res.ok) throw new Error(`HTTP ${res.status}`) return res.json() }) .then(data => { setData(data); setLoading(false) }) .catch(err => { if (err.name !== 'AbortError') { setError(err.message) setLoading(false) } }) return () => controller.abort() }, [url]) return { data, loading, error } } // Any component can use it: function MenuList() { const { data, loading, error } = useFetch('/api/menu') if (loading) return <Spinner /> if (error) return <Error message={error} /> return data.items.map(item => <MenuCard key={item.id} {...item} />) }
Your Challenge
Extract these patterns into custom hooks: (1) useDebounce(value, delay) — returns debounced value. (2) useMenuItems(category) — fetches and returns { items, loading, error }, re-fetches when category changes, cancels with AbortController. Use both in a SearchableMenu component that replaces 40+ lines of inline logic with 2 hook calls.
Challenge
Build a useMenuItems(category) hook that fetches menu data, returns { items, loading, error }, and re-fetches when category changes. Build a useDebounce(value, 400) hook. Use both in a SearchableMenu component.
Custom Hooks
mediumThree different menu components all fetch data, manage loading state, and handle errors in the same way. Instead of repeating the pattern, extract it into a useFetch hook — one place to maintain, used everywhere.
Custom Hooks
A custom hook is a function that starts with use and calls other hooks. That is the entire definition. It extracts stateful logic from a component so the same logic can be shared between multiple components without duplicating code.
Four Custom Hook Patterns
useLocalStorage, useDebounce, useOnline, usePrevious:
Delays updating a value until the user stops changing it. Prevents firing search API on every keystroke.
function useDebounce(value, delay = 300) { const [debounced, setDebounced] = useState(value) useEffect(() => { const timer = setTimeout(() => { setDebounced(value) }, delay) return () => clearTimeout(timer) // clear on next change }, [value, delay]) return debounced }
use can call other hooks. Plain functions cannot.The Three Hook Rules
Why these rules exist and what breaks when you violate them:
Only call hooks at the top level of your component or custom hook — not inside loops, conditions, or nested functions. React relies on the order of hook calls to associate state with the right hook.
// ❌ Conditional hook — BREAKS React's call order function Component({ show }) { if (show) { const [data, setData] = useState(null) // conditional! } const [name, setName] = useState('') // order shifts! }
// ✓ Hooks always at top level — consistent order function Component({ show }) { const [data, setData] = useState(null) // always called const [name, setName] = useState('') // always called // Move the condition INSIDE the component logic: if (!show) return null }
useFetch — A Production-Ready Example
The most common custom hook — data fetching with loading, error, and abort:
// The custom hook — reusable fetch logic function useFetch(url) { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { const controller = new AbortController() setLoading(true) setError(null) fetch(url, { signal: controller.signal }) .then(res => { if (!res.ok) throw new Error(`HTTP ${res.status}`) return res.json() }) .then(data => { setData(data); setLoading(false) }) .catch(err => { if (err.name !== 'AbortError') { setError(err.message) setLoading(false) } }) return () => controller.abort() }, [url]) return { data, loading, error } } // Any component can use it: function MenuList() { const { data, loading, error } = useFetch('/api/menu') if (loading) return <Spinner /> if (error) return <Error message={error} /> return data.items.map(item => <MenuCard key={item.id} {...item} />) }
Your Challenge
Extract these patterns into custom hooks: (1) useDebounce(value, delay) — returns debounced value. (2) useMenuItems(category) — fetches and returns { items, loading, error }, re-fetches when category changes, cancels with AbortController. Use both in a SearchableMenu component that replaces 40+ lines of inline logic with 2 hook calls.
Challenge
Build a useMenuItems(category) hook that fetches menu data, returns { items, loading, error }, and re-fetches when category changes. Build a useDebounce(value, 400) hook. Use both in a SearchableMenu component.