Hooks are the core of modern React. Here's each one explained with real examples — not contrived counters, but the kinds of patterns you'll actually use in production code.

useState — Local Component State

import { useState } from 'react'

function TemplateCard({ name, price }: { name: string; price: number }) {
  const [liked,      setLiked]      = useState(false)
  const [quantity,   setQuantity]   = useState(1)
  const [showDetail, setShowDetail] = useState(false)

  const total = price * quantity

  return (
    <div className="border rounded-2xl p-5 shadow-sm bg-white">
      <div className="flex justify-between items-start mb-3">
        <h3 className="font-bold text-lg">{name}</h3>
        <button
          onClick={() => setLiked(l => !l)}
          className={`text-xl ${liked ? 'text-red-500' : 'text-gray-300'}`}
        >
          ♥
        </button>
      </div>

      <div className="flex items-center gap-3 mb-4">
        <button onClick={() => setQuantity(q => Math.max(1, q - 1))}
          className="w-8 h-8 border rounded-lg">−</button>
        <span className="font-bold">{quantity}</span>
        <button onClick={() => setQuantity(q => q + 1)}
          className="w-8 h-8 border rounded-lg">+</button>
      </div>

      <p className="text-xl font-bold text-red-500 mb-3">${total}</p>

      <button
        onClick={() => setShowDetail(d => !d)}
        className="text-sm text-blue-600 underline"
      >
        {showDetail ? 'Hide' : 'Show'} details
      </button>

      {showDetail && (
        <p className="text-sm text-gray-500 mt-2">
          Angular 21 + Bootstrap 5 admin template with dark mode and RTL.
        </p>
      )}
    </div>
  )
}

State updater functions (setQuantity(q => q + 1)) are safer than direct values (setQuantity(quantity + 1)) because they always use the latest state, even in stale closures.

useEffect — Side Effects

import { useState, useEffect } from 'react'

function TemplateList() {
  const [templates, setTemplates] = useState<any[]>([])
  const [loading,   setLoading]   = useState(true)
  const [error,     setError]     = useState<string | null>(null)

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

    async function fetchTemplates() {
      try {
        const res  = await fetch('/api/templates')
        if (!res.ok) throw new Error(`HTTP ${res.status}`)
        const data = await res.json()
        if (!cancelled) {
          setTemplates(data)
          setLoading(false)
        }
      } catch (err) {
        if (!cancelled) {
          setError(err instanceof Error ? err.message : 'Failed to load')
          setLoading(false)
        }
      }
    }

    fetchTemplates()

    return () => { cancelled = true } // cleanup
  }, []) // empty array = run once on mount

  if (loading) return <div className="animate-pulse">Loading...</div>
  if (error)   return <div className="text-red-500">{error}</div>

  return (
    <div className="grid grid-cols-3 gap-4">
      {templates.map(t => <div key={t.id}>{t.name}</div>)}
    </div>
  )
}

useEffect dependency array:

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

useRef — DOM Access & Mutable Values

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

function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
  const inputRef    = useRef<HTMLInputElement>(null)
  const timerRef    = useRef<ReturnType<typeof setTimeout>>()
  const [query, setQuery] = useState('')

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

  // Debounce search — timerRef doesn't cause re-renders
  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    const value = e.target.value
    setQuery(value)
    clearTimeout(timerRef.current)
    timerRef.current = setTimeout(() => onSearch(value), 400)
  }

  return (
    <div className="relative">
      <input
        ref={inputRef}
        value={query}
        onChange={handleChange}
        placeholder="Search templates..."
        className="w-full border-2 rounded-xl px-4 py-2 focus:border-red-400 outline-none"
      />
      {query && (
        <button
          onClick={() => { setQuery(''); inputRef.current?.focus() }}
          className="absolute right-3 top-2.5 text-gray-400"
        >
          ✕
        </button>
      )}
    </div>
  )
}

useMemo — Expensive Computations

import { useState, useMemo } from 'react'

interface Template {
  id: number
  name: string
  framework: string
  price: number
  rating: number
}

function TemplateGrid({ templates }: { templates: Template[] }) {
  const [search,    setSearch]    = useState('')
  const [framework, setFramework] = useState('All')
  const [sortBy,    setSortBy]    = useState<'price' | 'rating'>('rating')

  // Filtered + sorted list — only recalculates when dependencies change
  const filtered = useMemo(() => {
    return templates
      .filter(t => framework === 'All' || t.framework === framework)
      .filter(t => t.name.toLowerCase().includes(search.toLowerCase()))
      .sort((a, b) => sortBy === 'price' ? a.price - b.price : b.rating - a.rating)
  }, [templates, search, framework, sortBy])

  // Summary stats — also memoized
  const stats = useMemo(() => ({
    total:   filtered.length,
    avgPrice: filtered.reduce((s, t) => s + t.price, 0) / (filtered.length || 1),
  }), [filtered])

  return (
    <div>
      <div className="flex gap-3 mb-4">
        <input value={search} onChange={e => setSearch(e.target.value)}
          placeholder="Search..." className="border rounded-lg px-3 py-2 flex-1" />
        <select value={framework} onChange={e => setFramework(e.target.value)}
          className="border rounded-lg px-3 py-2">
          <option>All</option>
          <option>Angular</option>
          <option>React</option>
          <option>Bootstrap</option>
        </select>
      </div>
      <p className="text-sm text-gray-500 mb-4">
        {stats.total} results · avg ${stats.avgPrice.toFixed(0)}
      </p>
      <div className="grid grid-cols-3 gap-4">
        {filtered.map(t => <div key={t.id} className="border rounded-xl p-4">{t.name}</div>)}
      </div>
    </div>
  )
}

useCallback — Stable Function References

import { useState, useCallback, memo } from 'react'

// Child wrapped in memo — only re-renders if props change
const TemplateRow = memo(function TemplateRow({
  template, onDelete, onEdit
}: {
  template: { id: number; name: string }
  onDelete: (id: number) => void
  onEdit:   (id: number) => void
}) {
  console.log('TemplateRow render', template.name)
  return (
    <div className="flex justify-between items-center p-3 border-b">
      <span>{template.name}</span>
      <div className="flex gap-2">
        <button onClick={() => onEdit(template.id)}
          className="text-sm text-blue-600">Edit</button>
        <button onClick={() => onDelete(template.id)}
          className="text-sm text-red-500">Delete</button>
      </div>
    </div>
  )
})

function TemplateManager() {
  const [templates, setTemplates] = useState([
    { id: 1, name: 'Marvel Dashboard' },
    { id: 2, name: 'Kiosk Angular' },
  ])

  // Without useCallback: new function on every render → memo is bypassed
  // With useCallback: same reference as long as [] dependencies don't change
  const handleDelete = useCallback((id: number) => {
    setTemplates(prev => prev.filter(t => t.id !== id))
  }, [])

  const handleEdit = useCallback((id: number) => {
    console.log('Editing', id)
  }, [])

  return (
    <div className="border rounded-2xl overflow-hidden">
      {templates.map(t => (
        <TemplateRow key={t.id} template={t}
          onDelete={handleDelete} onEdit={handleEdit} />
      ))}
    </div>
  )
}

Quick Reference

HookUse when
useStateLocal value that causes re-renders when changed
useEffectFetching data, subscriptions, DOM side effects
useRefDOM element access, or mutable value that doesn't cause re-renders
useMemoExpensive calculation that depends on specific values
useCallbackStable function reference passed to memoized children
useContextReading shared state without prop drilling
useReducerComplex state with multiple sub-values or actions
useIdGenerating unique IDs for accessibility labels

Frequently Asked Questions

Hooks are functions that let you use React state and lifecycle features inside functional components. They were introduced in React 16.8 to replace class components. All hooks start with 'use' — useState, useEffect, useRef, useMemo, etc.
useMemo caches a computed value: const total = useMemo(() => items.reduce(...), [items]). useCallback caches a function reference: const handleClick = useCallback(() => doSomething(id), [id]). Both only recalculate when dependencies change. Use them when the computation is expensive or when passing functions/values to child components that use React.memo.
Only call hooks at the top level of a React function — not inside loops, conditions, or nested functions. Only call hooks from React function components or custom hooks. These rules ensure React can reliably track hook state between renders.
useRef has two uses: accessing DOM elements directly (const inputRef = useRef(null), then <input ref={inputRef}>, then inputRef.current.focus()), and storing mutable values that don't trigger re-renders when changed (like timer IDs, previous values, or cached objects).

Need a Full Bootstrap 5 Admin Dashboard?

Get a complete Angular 21 + Bootstrap 5 dashboard with 50+ components — built by the same team behind BootstrapPlanet.

Browse Templates →

Use code FIRST30 for 30% off your first purchase.

Related Components