ReReact · Lesson 5 of 8

Custom Hooks

Custom hooks are functions that start with `use` and call other hooks. They let you extract stateful logic into reusable pieces — without the complexity of class components or render props.

JSX
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>
}
JSX
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>
  )
}
✦ Tip
The rule of hooks: call hooks at the top level of a component or custom hook — never inside conditions, loops, or nested functions. This ensures hooks run in the same order on every render, which is how React tracks which state belongs to which hook call.