Published on

React Hooks - The Complete Guide with Real-World Examples

Authors

Introduction

React Hooks transformed how we write React components. No more class components, no more this confusion — just clean, composable functions.

This guide covers every important hook with practical examples you'll use in real projects.

useState — Local Component State

import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
      <button onClick={() => setCount(c => c - 1)}>-</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  )
}

State with objects:

const [user, setUser] = useState({ name: '', email: '' })

// ✅ Spread to preserve other fields
setUser(prev => ({ ...prev, name: 'Alice' }))

useEffect — Side Effects

import { useState, useEffect } from 'react'

function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    let cancelled = false  // Prevent state update on unmounted component

    async function fetchUser() {
      setLoading(true)
      try {
        const res = await fetch(`/api/users/${userId}`)
        const data = await res.json()
        if (!cancelled) setUser(data)
      } finally {
        if (!cancelled) setLoading(false)
      }
    }

    fetchUser()

    return () => { cancelled = true }  // Cleanup function
  }, [userId])  // Re-run when userId changes

  if (loading) return <div>Loading...</div>
  return <div>{user?.name}</div>
}

useEffect dependency array:

useEffect(() => { /* runs once on mount */ }, [])
useEffect(() => { /* runs on every render */ })
useEffect(() => { /* runs when count changes */ }, [count])

useCallback — Memoize Functions

import { useState, useCallback } from 'react'

function SearchComponent() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])

  // Without useCallback, this creates a new function on every render
  // With useCallback, same function reference unless deps change
  const handleSearch = useCallback(async (searchQuery: string) => {
    const data = await searchAPI(searchQuery)
    setResults(data)
  }, [])  // No deps — function never changes

  return (
    <div>
      <input
        value={query}
        onChange={e => {
          setQuery(e.target.value)
          handleSearch(e.target.value)
        }}
      />
      {results.map(r => <div key={r.id}>{r.name}</div>)}
    </div>
  )
}

useMemo — Memoize Expensive Calculations

import { useMemo } from 'react'

function ProductList({ products, filter }) {
  // Only recalculates when products or filter changes
  const filteredProducts = useMemo(() =>
    products
      .filter(p => p.category === filter)
      .sort((a, b) => a.price - b.price),
    [products, filter]
  )

  return (
    <ul>
      {filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  )
}

Note: In 2026, the React Compiler handles most memoization automatically. Only use useMemo/useCallback for genuinely expensive operations.

useRef — Access DOM and Persist Values

import { useRef, useEffect } from 'react'

function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>(null)

  useEffect(() => {
    inputRef.current?.focus()  // Focus on mount
  }, [])

  return <input ref={inputRef} />
}

// Persist a value without triggering re-render
function Timer() {
  const intervalRef = useRef<NodeJS.Timeout | null>(null)
  const [count, setCount] = useState(0)

  function start() {
    intervalRef.current = setInterval(() => setCount(c => c + 1), 1000)
  }

  function stop() {
    if (intervalRef.current) clearInterval(intervalRef.current)
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  )
}

useContext — Global State Without Props Drilling

import { createContext, useContext, useState } from 'react'

// 1. Create context
interface ThemeContextType {
  theme: 'light' | 'dark'
  toggleTheme: () => void
}

const ThemeContext = createContext<ThemeContextType | null>(null)

// 2. Create provider
function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light')

  const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light')

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

// 3. Custom hook for safe usage
function useTheme() {
  const context = useContext(ThemeContext)
  if (!context) throw new Error('useTheme must be used within ThemeProvider')
  return context
}

// 4. Use anywhere in the tree
function Header() {
  const { theme, toggleTheme } = useTheme()
  return (
    <header className={theme}>
      <button onClick={toggleTheme}>Toggle {theme}</button>
    </header>
  )
}

useReducer — Complex State Logic

import { useReducer } from 'react'

type State = { count: number; step: number }
type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset' }
  | { type: 'setStep'; payload: number }

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment': return { ...state, count: state.count + state.step }
    case 'decrement': return { ...state, count: state.count - state.step }
    case 'reset': return { ...state, count: 0 }
    case 'setStep': return { ...state, step: action.payload }
  }
}

function AdvancedCounter() {
  const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 })

  return (
    <div>
      <p>Count: {state.count}</p>
      <input
        type="number"
        value={state.step}
        onChange={e => dispatch({ type: 'setStep', payload: +e.target.value })}
      />
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  )
}

Building Custom Hooks

Custom hooks let you extract and reuse stateful logic:

// useLocalStorage — persist state in localStorage
function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch {
      return initialValue
    }
  })

  const setStoredValue = (newValue: T) => {
    setValue(newValue)
    window.localStorage.setItem(key, JSON.stringify(newValue))
  }

  return [value, setStoredValue] as const
}

// Usage
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light')
  return <button onClick={() => setTheme('dark')}>Dark mode</button>
}
// useDebounce — delay state updates
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value)

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

  return debouncedValue
}

// Usage
function SearchBox() {
  const [query, setQuery] = useState('')
  const debouncedQuery = useDebounce(query, 500)  // Wait 500ms after typing

  useEffect(() => {
    if (debouncedQuery) searchAPI(debouncedQuery)
  }, [debouncedQuery])

  return <input value={query} onChange={e => setQuery(e.target.value)} />
}

Conclusion

Hooks are the heart of modern React. Master useState, useEffect, useRef, and useContext first — they cover 90% of use cases. Then build custom hooks to extract and share logic across components. That's the real superpower: composable, reusable, testable React logic.