Custom Hooks
Introduction
A custom hook is a function named useSomething that calls built-in hooks to encapsulate reusable stateful logic. Share useCounter, useFetch, and useLocalStorage across components without copy-paste or deep prop chains.
Prerequisites
Rules of Hooks
| Rule | Meaning |
|---|---|
| Top level only | Do not call hooks inside loops, conditions, or nested functions |
| React functions only | Call hooks from components or other custom hooks |
| Stable order | Same hooks in the same order every render |
ESLint eslint-plugin-react-hooks enforces these rules.
Unlike Vue composables, React hooks cannot be called conditionally—see Vue Composables comparison.
useCounter
src/hooks/useCounter.ts:
CounterPanel.tsx:
useFetch
src/hooks/useFetch.ts:
Usage:
const { data, loading, error } = useFetch<Health>("/api/health");Full API patterns in Fetching API and CORS—blog project uses usePosts / usePost.
useLocalStorage
Warning
Sensitive Data
Do not store JWT in localStorage for high-security apps without understanding XSS—see admin auth project.
Testing Hooks
With @testing-library/react:
import { renderHook, act } from "@testing-library/react";
import { useCounter } from "./useCounter";
test("increments", () => {
const { result } = renderHook(() => useCounter(0));
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});Details in Testing React Components.
File Layout
src/hooks/
├── useCounter.ts
├── useFetch.ts
├── usePosts.ts # later — blog project
└── useLocalStorage.tsFAQ
Hook vs utility function?
If it uses useState, useEffect, etc.—it must be a hook (use prefix).
Pass parameters?
export function usePost(slug: string) { ... }Re-run effects when slug changes.
Share hook state between components?
Each call creates independent state. For shared state use Zustand or Context.
Extract hook from component?
Select lines using hooks → move to useXxx.ts → return values and handlers.