Frontend Master

JavaScript Curriculum

Custom Hooks
+60 XP

Custom Hooks

medium
~30 min·60 XP

Three 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:

custom-hooks.jsx

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
}
Live demo
input:""
debounced:""
Debounced value updates 500ms after you stop typing. API call only fires once.
Rule: Hooks starting with use can call other hooks. Plain functions cannot.
ℹ️Custom hooks share logic, not state
Each component that calls a custom hook gets its own isolated state. Calling `useLocalStorage('theme', 'dark')` in two components gives two independent state variables — they both happen to read from/write to the same localStorage key, but the state is not shared.

The Three Hook Rules

Why these rules exist and what breaks when you violate them:

hook-rules.jsx

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.

❌ Wrong
// ❌ Conditional hook — BREAKS React's call order
function Component({ show }) {
  if (show) {
    const [data, setData] = useState(null) // conditional!
  }
  const [name, setName] = useState('') // order shifts!
}
✓ Correct
// ✓ 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
}
Why: React relies on hook call order to match state to hooks. A conditional hook changes the order — subsequent hooks get the wrong state.
💡eslint-plugin-react-hooks enforces the rules
Install `eslint-plugin-react-hooks` and add it to your ESLint config. It automatically detects hook rule violations — conditional hooks, hooks in loops, missing 'use' prefix. It also enforces exhaustive-deps for useEffect.

useFetch — A Production-Ready Example

The most common custom hook — data fetching with loading, error, and abort:

useFetch.jsx
// 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} />)
}
Simulate useFetch(url)
loading:false
error:null
data: null
⚠️Custom hooks are not magic — they are just functions
A custom hook runs inline in your component. It is not a separate instance. If the hook throws, your component throws. If the hook re-renders, it is because the component re-rendered. Debugging is the same as debugging any function call.

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-hooksuseDebounceuseLocalStorageuseFetchusePrevioushook-rulesextracting-logicreusable-hooksuse-prefix
Custom Hooks | Nexus Learn