Custom Hooks
Custom hooks pattern for extracting and reusing stateful logic across React components
You are an expert in the Custom Hooks pattern for building React applications. ## Key Points - **Naming Convention**: Must start with `use` so React can enforce the Rules of Hooks. - **Composition**: Custom hooks compose — a hook can call other custom hooks. - **Isolation**: Each call to a hook gets its own independent state; hooks do not share state between components unless they read from an external store. - **Return Value Flexibility**: A hook can return a single value, a tuple, or an object — choose based on ergonomics. - Return tuples (`[value, setter]`) for hooks with one primary value and one updater; return objects for hooks with many fields. - Accept configuration as a single options object when a hook takes more than two parameters. - Handle cleanup in `useEffect` return functions — cancel fetches, clear timers, remove listeners. - Write hooks as pure logic — avoid rendering JSX inside a hook. - Provide TypeScript generics for hooks that wrap user-defined data shapes. - Write unit tests using `renderHook` from `@testing-library/react`. - **Violating the Rules of Hooks**: Never call hooks conditionally or inside loops. Keep all hook calls at the top level of the function body. - **Stale closures**: When an effect or callback captures a value that changes over time, use refs or include it in the dependency array.
skilldb get react-patterns-skills/Custom HooksFull skill: 189 linesCustom Hooks — React Patterns
You are an expert in the Custom Hooks pattern for building React applications.
Overview
Custom hooks are JavaScript functions whose names start with use that encapsulate stateful logic built on top of React's built-in hooks. They let you extract component logic into reusable, testable units without changing the component hierarchy. A custom hook can call other hooks, manage local state, trigger effects, and return any value the consumer needs.
Core Philosophy
Custom hooks are React's answer to the oldest problem in software engineering: how do you share logic without sharing UI? Before hooks, the answer involved higher-order components and render props — both of which worked but distorted the component tree, made debugging harder, and tangled the ownership of props. Custom hooks solve this cleanly: extract the logic into a function, call it from any component, and each call gets its own independent state.
The power of custom hooks comes from composition. A useAuth hook might internally use useState, useEffect, and useContext. A useProtectedRoute hook might call useAuth and useRouter. Each hook is a self-contained unit of behavior that composes with others the same way functions compose in any programming language. This is not an accident — hooks are functions, and they inherit all the benefits of functional composition: testability, readability, and the ability to build complex behavior from simple pieces.
The discipline is knowing when to extract. Not every useState and useEffect pair deserves its own hook. Extract when you see the same stateful logic repeated across components, when a component's logic is complex enough to obscure its rendering intent, or when you want to test the logic independently of the UI. A good custom hook has a clear name that describes what it does, a minimal parameter list, and a return value that makes the consumer's code more readable, not less.
Core Concepts
- Naming Convention: Must start with
useso React can enforce the Rules of Hooks. - Composition: Custom hooks compose — a hook can call other custom hooks.
- Isolation: Each call to a hook gets its own independent state; hooks do not share state between components unless they read from an external store.
- Return Value Flexibility: A hook can return a single value, a tuple, or an object — choose based on ergonomics.
Implementation Patterns
useLocalStorage
import { useState, useEffect } from "react";
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch {
// Storage full or unavailable
}
}, [key, storedValue]);
return [storedValue, setStoredValue] as const;
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage<"light" | "dark">("theme", "light");
return <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>{theme}</button>;
}
useFetch
import { useState, useEffect, useRef } from "react";
interface UseFetchResult<T> {
data: T | null;
error: Error | null;
loading: boolean;
refetch: () => void;
}
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);
const abortRef = useRef<AbortController | null>(null);
const fetchData = () => {
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
setLoading(true);
setError(null);
fetch(url, { signal: controller.signal })
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then((json) => {
setData(json as T);
setLoading(false);
})
.catch((err) => {
if (err.name !== "AbortError") {
setError(err);
setLoading(false);
}
});
};
useEffect(() => {
fetchData();
return () => abortRef.current?.abort();
}, [url]);
return { data, error, loading, refetch: fetchData };
}
useDebounce
import { useState, useEffect } from "react";
function useDebounce<T>(value: T, delayMs: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delayMs);
return () => clearTimeout(timer);
}, [value, delayMs]);
return debounced;
}
// Usage
function Search() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 300);
const { data } = useFetch<SearchResult[]>(`/api/search?q=${debouncedQuery}`);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
useMediaQuery
import { useState, useEffect } from "react";
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(() => window.matchMedia(query).matches);
useEffect(() => {
const mql = window.matchMedia(query);
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
mql.addEventListener("change", handler);
setMatches(mql.matches);
return () => mql.removeEventListener("change", handler);
}, [query]);
return matches;
}
Best Practices
- Return tuples (
[value, setter]) for hooks with one primary value and one updater; return objects for hooks with many fields. - Accept configuration as a single options object when a hook takes more than two parameters.
- Handle cleanup in
useEffectreturn functions — cancel fetches, clear timers, remove listeners. - Write hooks as pure logic — avoid rendering JSX inside a hook.
- Provide TypeScript generics for hooks that wrap user-defined data shapes.
- Write unit tests using
renderHookfrom@testing-library/react.
Common Pitfalls
- Violating the Rules of Hooks: Never call hooks conditionally or inside loops. Keep all hook calls at the top level of the function body.
- Stale closures: When an effect or callback captures a value that changes over time, use refs or include it in the dependency array.
- Missing cleanup: Forgetting to abort fetches or clear timers causes state updates on unmounted components.
- Returning unstable references: Returning a new object or array on every render defeats
React.memo. UseuseMemooruseCallbackfor returned values that consumers might depend on in their own dependency arrays. - Overly generic hooks: A hook that tries to do everything becomes hard to use. Prefer small, focused hooks that compose well.
Anti-Patterns
-
The "useEverything" kitchen-sink hook: Creating a single hook that manages form state, validation, API calls, and toast notifications. These hooks accumulate parameters, return enormous objects, and become impossible to test or reuse because every consumer is coupled to every concern.
-
Hooks that return JSX: Writing a custom hook that returns rendered elements (e.g.,
const { modal, openModal } = useModal()). This blurs the line between hooks and components, makes the rendering order unpredictable, and prevents consumers from controlling where the JSX appears in the tree. -
Ignoring the dependency array contract: Omitting dependencies from
useEffectoruseMemoarrays to "prevent re-runs" instead of restructuring the code. This creates stale closures that read outdated values and produce bugs that only manifest under specific timing conditions. -
Wrapping a single
useStatein a custom hook: Extractingconst [count, setCount] = useState(0)intouseCount()adds a layer of indirection with no benefit. Custom hooks earn their existence by combining multiple primitives or encapsulating non-trivial logic. -
Conditionally calling hooks: Wrapping hook calls in
ifstatements or returning early before all hooks have been called. This violates the Rules of Hooks, causes React to misalign hook state between renders, and produces cryptic errors that are hard to diagnose.
Install this skill directly: skilldb add react-patterns-skills
Related Skills
Compound Components
Compound component pattern for building flexible, implicitly-shared React component APIs
Context Patterns
React Context patterns for efficient state sharing, provider composition, and avoiding unnecessary re-renders
Error Boundaries
Error boundary pattern for gracefully catching and recovering from runtime errors in React component trees
Optimistic Updates
Optimistic update patterns for instant UI feedback with server reconciliation and rollback in React
Render Props
Render props pattern for sharing cross-cutting logic through function-as-children and render callbacks
Server Components
React Server Components pattern for zero-bundle server-rendered components with direct backend access