Custom Hooks

Med

Custom hooks extract reusable stateful logic into functions that start with "use". They allow you to share behavior between components without changing the component hierarchy. Each component calling a custom hook gets its own independent copy of the state.

Interactive Visualization

Key Points

  • Must start with "use" to follow the rules of hooks
  • Can call other hooks inside them including useState, useEffect, and other custom hooks
  • Each component using the hook gets independent state
  • Replace render props and HOCs for sharing stateful logic
  • Should do one thing well and be composable with other hooks
  • Test by testing the component that uses them or with renderHook from testing library

Code Examples

useLocalStorage Hook

import { useState, useEffect } from 'react'

function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
  const [stored, setStored] = useState<T>(() => {
    try {
      const item = localStorage.getItem(key)
      return item ? JSON.parse(item) as T : initialValue
    } catch {
      return initialValue
    }
  })

  useEffect(() => {
    try {
      localStorage.setItem(key, JSON.stringify(stored))
    } catch {
      // Storage full or unavailable
    }
  }, [key, stored])

  return [stored, setStored]
}

function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'dark')
  const [fontSize, setFontSize] = useLocalStorage('fontSize', 16)

  return (
    <div>
      <select value={theme} onChange={(e) => setTheme(e.target.value)}>
        <option value="dark">Dark</option>
        <option value="light">Light</option>
      </select>
      <input
        type="range"
        value={fontSize}
        onChange={(e) => setFontSize(Number(e.target.value))}
      />
    </div>
  )
}

The hook encapsulates localStorage read/write logic that any component can reuse, each getting its own independent value

useDebounce Hook

import { useState, useEffect } from 'react'

function useDebounce<T>(value: T, delay: number): T {
  const [debounced, setDebounced] = useState<T>(value)

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay)
    return () => clearTimeout(timer)
  }, [value, delay])

  return debounced
}

function SearchBar() {
  const [input, setInput] = useState('')
  const debouncedQuery = useDebounce(input, 300)

  useEffect(() => {
    if (debouncedQuery) {
      // Fetch results only after user stops typing
      fetch(`/api/search?q=${debouncedQuery}`)
    }
  }, [debouncedQuery])

  return (
    <input
      value={input}
      onChange={(e) => setInput(e.target.value)}
      placeholder="Search..."
    />
  )
}

useDebounce delays updating the returned value until the input has been stable for the specified delay, reducing API calls

useMediaQuery Hook

import { useState, useEffect } from 'react'

function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(() =>
    typeof window !== 'undefined'
      ? window.matchMedia(query).matches
      : false
  )

  useEffect(() => {
    const mql = window.matchMedia(query)
    const handler = (e: MediaQueryListEvent) => setMatches(e.matches)

    mql.addEventListener('change', handler)
    setMatches(mql.matches)
    return () => mql.removeEventListener('change', handler)
  }, [query])

  return matches
}

function ResponsiveNav() {
  const isMobile = useMediaQuery('(max-width: 768px)')
  return isMobile ? <HamburgerMenu /> : <DesktopNav />
}

The hook subscribes to a CSS media query, returning a reactive boolean that updates when the viewport crosses the breakpoint

Common Mistakes

  • Not starting the function name with "use" which disables lint rules for hook ordering
  • Assuming custom hooks share state between components that call them
  • Creating custom hooks that do too many things instead of composable single-purpose hooks

Interview Tips

  • Explain that custom hooks share logic but not state between components
  • Compare custom hooks to HOCs and render props as patterns for code reuse
  • Be ready to write a custom hook from scratch such as useFetch or useDebounce

Related Concepts