Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/rijvi-mahmud/shaddy/llms.txt

Use this file to discover all available pages before exploring further.

Before React 16.8, sharing stateful logic between components required Higher-Order Components or Render Props — both of which add wrapper components to the tree and obscure what is actually happening. Custom hooks solve this more directly: extract the logic into a plain function, call it from any component, and get the same behaviour without any extra nesting.

What a Custom Hook Is

A custom hook is a JavaScript function whose name starts with use and that can call other hooks internally. It can accept arguments, manage state, register effects, and return whatever values or functions a component needs.
// The three rules a custom hook follows:
// 1. Name starts with "use"
// 2. Can call other hooks (useState, useEffect, etc.)
// 3. Returns values or functions the component uses

Before and After Extraction

Before — fetch logic lives inside the component, which makes it impossible to reuse and harder to test:
// UserProfile.tsx — before
import { useState, useEffect } from 'react'

export function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    setLoading(true)
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => { setUser(data); setLoading(false) })
      .catch(() => { setError('Failed to fetch user'); setLoading(false) })
  }, [userId])

  if (loading) return <div>Loading...</div>
  if (error) return <div>{error}</div>
  return <div>{user?.name}</div>
}
After — the fetch logic moves into a reusable hook:
// hooks/useFetch.ts
import { useState, useEffect } from 'react'

interface UseFetchResult<T> {
  data: T | null
  loading: boolean
  error: string | null
  refetch: () => void
}

export function useFetch<T>(url: string): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  const fetchData = async () => {
    try {
      setLoading(true)
      setError(null)
      const response = await fetch(url)
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      const result = await response.json()
      setData(result)
    } catch (err) {
      setError(err instanceof Error ? err.message : 'An error occurred')
    } finally {
      setLoading(false)
    }
  }

  useEffect(() => {
    fetchData()
  }, [url])

  return { data, loading, error, refetch: fetchData }
}
// UserProfile.tsx — after
import { useFetch } from './hooks/useFetch'

interface User {
  id: number
  name: string
  email: string
  avatar: string
}

export function UserProfile({ userId }: { userId: number }) {
  const { data: user, loading, error, refetch } = useFetch<User>(`/api/users/${userId}`)

  if (loading) return <div className="text-center p-4">Loading user...</div>

  if (error) {
    return (
      <div className="text-red-500 p-4">
        <p>Error: {error}</p>
        <button onClick={refetch} className="mt-2 px-4 py-2 bg-blue-500 text-white rounded">
          Retry
        </button>
      </div>
    )
  }

  if (!user) return <div className="p-4">No user found</div>

  return (
    <div className="p-6 border rounded-lg">
      <img src={user.avatar} alt={user.name} className="w-20 h-20 rounded-full mb-4" />
      <h2 className="text-2xl font-bold">{user.name}</h2>
      <p className="text-gray-600">{user.email}</p>
    </div>
  )
}

Custom Hook for Form Input

A simple hook that manages a single form field, including reset:
// hooks/useInput.ts
import { useState, ChangeEvent } from 'react'

export function useInput(initialValue: string = '') {
  const [value, setValue] = useState(initialValue)

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value)
  }

  const reset = () => {
    setValue(initialValue)
  }

  return { value, onChange: handleChange, reset }
}
// LoginForm.tsx
import { useInput } from './hooks/useInput'

export function LoginForm() {
  const email = useInput('')
  const password = useInput('')

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    console.log('Email:', email.value)
    console.log('Password:', password.value)
    email.reset()
    password.reset()
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <input type="email" placeholder="Email" {...email} className="w-full px-4 py-2 border rounded" />
      <input type="password" placeholder="Password" {...password} className="w-full px-4 py-2 border rounded" />
      <button type="submit" className="w-full px-4 py-2 bg-blue-500 text-white rounded">
        Login
      </button>
    </form>
  )
}

Composing Hooks

Hooks compose naturally — one hook can call another. Here a useTheme hook builds on useLocalStorage:
// hooks/useLocalStorage.ts
import { useState, useEffect } from 'react'

export 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
    }
  })

  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value))
    } catch (error) {
      console.error('Error saving to localStorage:', error)
    }
  }, [key, value])

  return [value, setValue] as const
}
// hooks/useTheme.ts
import { useLocalStorage } from './useLocalStorage'
import { useEffect } from 'react'

type Theme = 'light' | 'dark'

export function useTheme() {
  const [theme, setTheme] = useLocalStorage<Theme>('theme', 'light')

  useEffect(() => {
    document.documentElement.classList.toggle('dark', theme === 'dark')
  }, [theme])

  const toggleTheme = () => {
    setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))
  }

  return { theme, setTheme, toggleTheme }
}

Hook Composition Patterns

Data + Actions — return both state and functions to modify it:
function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue)

  const increment = () => setCount((c) => c + 1)
  const decrement = () => setCount((c) => c - 1)
  const reset = () => setCount(initialValue)

  return { count, increment, decrement, reset }
}
Configuration object — accept an options object for flexibility:
function usePagination<T>(items: T[], config = { itemsPerPage: 10 }) {
  const [currentPage, setCurrentPage] = useState(1)
  const { itemsPerPage } = config

  const totalPages = Math.ceil(items.length / itemsPerPage)
  const currentItems = items.slice(
    (currentPage - 1) * itemsPerPage,
    currentPage * itemsPerPage
  )

  return {
    currentItems,
    currentPage,
    totalPages,
    nextPage: () => setCurrentPage((p) => Math.min(p + 1, totalPages)),
    prevPage: () => setCurrentPage((p) => Math.max(p - 1, 1)),
    goToPage: setCurrentPage,
  }
}

Benefits

  • Reusability — write the logic once, use it in any component.
  • Separation of concerns — components stay focused on rendering; hooks own the behaviour.
  • Testability — hooks can be tested independently using @testing-library/react-hooks.
  • Composability — hooks call other hooks to build up complex behaviour from simple pieces.
  • Readability — component bodies become shorter and easier to follow.
  • Type safety — TypeScript types flow naturally through hook parameters and return values.

When to Use

  • The same stateful logic appears in two or more components.
  • A component has complex logic that distracts from its rendering purpose.
  • You want to test stateful behaviour independently from the UI.
  • Multiple components need to interact with the same external system (an API, localStorage, a WebSocket, etc.).

When Not to Use

  • The logic is only used in a single component and extraction adds no clarity.
  • The abstraction introduces more confusion than it removes.
  • The hook would have too many parameters or return too many values to be comprehensible.
  • You’re simply wrapping a single built-in hook without adding any value.
  • The logic is purely synchronous and stateless — use a regular utility function instead.
Always prefix custom hooks with use. This is required for React’s linting rules to correctly enforce the Rules of Hooks within your custom hooks.

Build docs developers (and LLMs) love