React logoReactv19ADVANCED

React Custom Hooks

React custom hooks cheat sheet with patterns for state management, data fetching, debounce, and reusable logic with code examples.

20 min read
reactcustom-hookshooksuseEffectuseStatedata-fetchingperformanceutilities

Sign in to mark items as known and track your progress.

Sign in

Custom Hook Fundamentals

Learn the fundamentals of creating reusable custom hooks, from basic patterns to advanced composition techniques

Creating Custom Hooks

Master the art of creating reusable custom hooks for state management, side effects, and logic encapsulation

jsx
// Basic Custom Hook Pattern
import { useState, useEffect } from 'react'

// useCounter - Simple state management hook
function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue)
  
  const increment = () => setCount(prev => prev + 1)
  const decrement = () => setCount(prev => prev - 1)
  const reset = () => setCount(initialValue)
  
  return { count, increment, decrement, reset }
}

// Using the hook
function Counter() {
  const { count, increment, decrement, reset } = useCounter(0)
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  )
}
📄 Example
// Complete Counter Component
function CounterApp() {
  const counter1 = useCounter(0)
  const counter2 = useCounter(100)
  
  return (
    <div>
      <div>
        <h3>Counter 1: {counter1.count}</h3>
        <button onClick={counter1.increment}>+</button>
        <button onClick={counter1.decrement}>-</button>
        <button onClick={counter1.reset}>Reset</button>
      </div>
      
      <div>
        <h3>Counter 2: {counter2.count}</h3>
        <button onClick={counter2.increment}>+</button>
        <button onClick={counter2.decrement}>-</button>
        <button onClick={counter2.reset}>Reset</button>
      </div>
    </div>
  )
}
🟢 Always prefix custom hooks with "use" to follow React conventions
💡 Custom hooks let you extract and reuse stateful logic between components
⚡ Return consistent data types from your hooks for predictable behavior
📌 Custom hooks can call other hooks, enabling powerful composition
⚠️ Follow the Rules of Hooks: only call at top level, not in conditions
state-managementreusabilitycompositionbest-practices

Advanced Hook Patterns

Explore advanced patterns including hook composition, factory hooks, and complex state management

jsx
// Hook with cleanup
function useInterval(callback, delay) {
  const savedCallback = useRef();
  
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);
  
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    
    if (delay !== null) {
      const id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

// Hook with dependencies
function useFetch(url, options) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const abortController = new AbortController();
    
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url, {
          ...options,
          signal: abortController.signal
        });
        const result = await response.json();
        setData(result);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err);
        }
      } finally {
        setLoading(false);
      }
    };
    
    fetchData();
    
    return () => abortController.abort();
  }, [url, JSON.stringify(options)]);
  
  return { data, loading, error };
}
⏱️ Use debounce for search inputs and API calls
🔄 Implement cleanup in effects to prevent memory leaks
📱 Use media query hooks for responsive behavior

Data Fetching Hooks

Build robust data fetching hooks with loading states, error handling, caching, and request cancellation

API Integration Hooks

Build production-ready data fetching hooks with caching, error handling, retries, and optimistic updates

jsx
// Basic fetch hook
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };
    
    fetchData();
  }, [url]);
  
  return { data, loading, error };
}
🚀 Implement request cancellation to prevent memory leaks
💾 Use caching to reduce redundant API calls
⚡ Add retry logic with exponential backoff for reliability

Utility & Performance Hooks

Create utility hooks for common tasks like local storage, debouncing, and performance optimization

Performance Optimization Hooks

Optimize React app performance with debouncing, throttling, memoization, and lazy loading hooks

jsx
// Memoization hook
function useMemoizedValue(fn, dependencies) {
  return useMemo(fn, dependencies);
}

// Callback memoization
function useStableCallback(callback, dependencies) {
  return useCallback(callback, dependencies);
}

// Ref for DOM access
function useElementRef() {
  const ref = useRef(null);
  
  const focus = () => ref.current?.focus();
  const blur = () => ref.current?.blur();
  const scrollIntoView = () => ref.current?.scrollIntoView();
  
  return { ref, focus, blur, scrollIntoView };
}
👁️ Use Intersection Observer for lazy loading and animations
📋 Implement clipboard functionality with fallbacks
⌨️ Create keyboard shortcuts for better UX

Browser & Storage Hooks

Sync React state with localStorage, media queries, and theme

useLocalStorage

State that persists to localStorage with cross-tab sync

jsx
import { useState, useEffect, useCallback } from 'react'

function useLocalStorage<T>(key: string, initial: T) {
  const [value, setValue] = useState<T>(() => {
    if (typeof window === 'undefined') return initial
    try {
      const raw = localStorage.getItem(key)
      return raw ? (JSON.parse(raw) as T) : initial
    } catch {
      return initial
    }
  })

  useEffect(() => {
    try {
      localStorage.setItem(key, JSON.stringify(value))
    } catch {}
  }, [key, value])

  return [value, setValue] as const
}

// Usage
const [theme, setTheme] = useLocalStorage('theme', 'light')
💡 Lazy init reads from localStorage once - no re-parse on every render
📌 The `storage` event fires in OTHER tabs only - gives you cross-tab sync
⚠️ Guard for SSR (typeof window) - Next.js will crash without it
🎯 Wrap in try/catch - quota errors and private mode both throw
storagestatepersistence

useMediaQuery

Subscribe to a CSS media query - responsive logic in JS

jsx
import { useSyncExternalStore } from 'react'

function useMediaQuery(query: string) {
  return useSyncExternalStore(
    (cb) => {
      const mql = window.matchMedia(query)
      mql.addEventListener('change', cb)
      return () => mql.removeEventListener('change', cb)
    },
    () => window.matchMedia(query).matches,
    () => false,           // SSR fallback
  )
}

// Usage
const isMobile = useMediaQuery('(max-width: 768px)')
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)')
const reducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)')
💡 useSyncExternalStore is the right primitive - no tearing, SSR-safe
⚡ Return false from getServerSnapshot so SSR renders the desktop view by default
📌 Honor `prefers-reduced-motion` and `prefers-color-scheme` - free accessibility wins
🎯 Wrap breakpoint strings in a `breakpoints` map so they stay in sync with Tailwind
responsivecssa11y

Interaction & Lifecycle Hooks

Practical hooks for debounce, click-outside, event listeners, and prior values

useDebouncedValue & useDebouncedCallback

Debounce a changing value or a callback - search inputs, autosave

jsx
import { useEffect, useState, useRef, useCallback } from 'react'

function useDebouncedValue<T>(value: T, delay = 300) {
  const [debounced, setDebounced] = useState(value)
  useEffect(() => {
    const t = setTimeout(() => setDebounced(value), delay)
    return () => clearTimeout(t)
  }, [value, delay])
  return debounced
}

// Usage
function Search() {
  const [q, setQ] = useState('')
  const debouncedQ = useDebouncedValue(q, 300)
  useEffect(() => {
    if (debouncedQ) fetch('/api/search?q=' + debouncedQ)
  }, [debouncedQ])
  return <input value={q} onChange={(e) => setQ(e.target.value)} />
}
💡 useDebouncedValue for derived state; useDebouncedCallback for side effects
⚡ The fnRef pattern keeps the debounced wrapper stable as fn changes
📌 Always cancel the pending timer on unmount - stale fires can crash
🎯 Typical delays: 200-300ms for search, 500-800ms for autosave
performancedebouncesearch

useClickOutside & useEventListener

Dismiss menus/popovers and attach typed event listeners safely

jsx
import { useEffect, useRef, RefObject } from 'react'

function useClickOutside<T extends HTMLElement>(
  ref: RefObject<T>,
  onOutside: (e: MouseEvent | TouchEvent) => void,
) {
  useEffect(() => {
    const handler = (e: MouseEvent | TouchEvent) => {
      const el = ref.current
      if (!el || el.contains(e.target as Node)) return
      onOutside(e)
    }
    document.addEventListener('mousedown', handler)
    document.addEventListener('touchstart', handler)
    return () => {
      document.removeEventListener('mousedown', handler)
      document.removeEventListener('touchstart', handler)
    }
  }, [ref, onOutside])
}

// Usage
function Dropdown() {
  const ref = useRef<HTMLDivElement>(null)
  const [open, setOpen] = useState(false)
  useClickOutside(ref, () => setOpen(false))
  return <div ref={ref}>{open && <Menu />}</div>
}
💡 Use mousedown (not click) - fires before focus/blur and feels snappier
⚡ The handlerRef pattern avoids re-binding listeners every render
📌 touchstart needs { passive: true } to avoid scroll-blocking warnings
🎯 useEventListener is the typed, cleanup-safe primitive for any DOM event
eventsdomui

usePrevious & useIsFirstRender

Track the previous value of a variable, detect the first render

jsx
import { useRef, useEffect } from 'react'

function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T | undefined>(undefined)
  useEffect(() => { ref.current = value }, [value])
  return ref.current
}

// Usage
function Counter({ count }: { count: number }) {
  const prev = usePrevious(count)
  return <p>Now: {count}, was: {prev ?? '-'}</p>
}
💡 usePrevious is two lines: a ref + effect - no library needed
📌 useUpdateEffect skips the mount run - perfect for "fire on change" UX
⚡ useIsFirstRender returns true exactly once per component lifetime
🎯 Combine usePrevious + a CSS class for cheap value-change flash effects
lifecyclepatterns

useIntersectionObserver

Detect when an element enters the viewport - lazy load, infinite scroll

jsx
import { useEffect, useRef, useState } from 'react'

function useIntersectionObserver(options?: IntersectionObserverInit) {
  const ref = useRef<HTMLElement>(null)
  const [entry, setEntry] = useState<IntersectionObserverEntry>()
  useEffect(() => {
    const el = ref.current
    if (!el) return
    const io = new IntersectionObserver(([e]) => setEntry(e), options)
    io.observe(el)
    return () => io.disconnect()
  }, [options?.root, options?.rootMargin, options?.threshold])
  return { ref, entry, isIntersecting: !!entry?.isIntersecting }
}

// Usage
function LazyImage({ src, alt }: { src: string; alt: string }) {
  const { ref, isIntersecting } = useIntersectionObserver({ rootMargin: '200px' })
  return (
    <div ref={ref as any}>
      {isIntersecting ? <img src={src} alt={alt} /> : <div className="placeholder" />}
    </div>
  )
}
💡 IntersectionObserver beats scroll listeners - browser-optimized, throttled by default
⚡ `rootMargin: "200px"` pre-fetches BEFORE the element is on screen
📌 freezeOnceVisible prevents re-triggering for one-shot animations
🎯 Sentinel + this hook is the cleanest infinite-scroll pattern in React
scrolllazy-loadperformance