Custom Hooks
Custom hooks extract reusable stateful logic; they share logic not state—each call gets isolated state.
A custom hook is a function that calls other hooks to package reusable stateful logic. It's the primary mechanism for reuse in modern React—replacing the old HOC and render-prop patterns for logic sharing.
The rules 🎯#
- Name must start with
use. This is how the linter and React know to apply the Rules of Hooks (call hooks unconditionally, at the top level only). - A custom hook may call
useState,useEffect,useRef, other custom hooks—anything a component can. - Return whatever shape is ergonomic: a tuple
[value, setter](likeuseState), or an object{ data, error, loading }for many named fields.
Logic is shared, state is NOT 🎯#
Each call to a custom hook gets its own isolated state. Two components (or two calls in one component) using useToggle do not share a value—the hook shares the recipe, not the data.
function useToggle(initial = false) {
const [on, setOn] = useState(initial);
const toggle = useCallback(() => setOn(o => !o), []);
return [on, toggle];
}
function Panel() {
const [open, toggleOpen] = useToggle(); // independent state
const [muted, toggleMuted] = useToggle(true); // independent state
// …
}
flowchart TD
H["useToggle (logic recipe)"] --> C1["Call in Panel: open state"]
H --> C2["Call in Panel: muted state"]
H --> C3["Call in Sidebar: collapsed state"]
C1 --- N1["isolated"]
C2 --- N2["isolated"]
C3 --- N3["isolated"]
Canonical examples#
useLocalStorage — state synced to localStorage:
function useLocalStorage(key, initial) {
const [value, setValue] = useState(() => {
const raw = localStorage.getItem(key);
return raw ? JSON.parse(raw) : initial;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
useDebounce — a value that lags behind rapid changes:
function useDebounce(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id); // ⚠️ cleanup cancels the pending timer
}, [value, delay]);
return debounced;
}
useFetch — data fetching with abort:
function useFetch(url) {
const [state, setState] = useState({ data: null, error: null, loading: true });
useEffect(() => {
const controller = new AbortController();
setState({ data: null, error: null, loading: true });
fetch(url, { signal: controller.signal })
.then(r => r.json())
.then(data => setState({ data, error: null, loading: false }))
.catch(error => {
if (error.name !== "AbortError") setState({ data: null, error, loading: false });
});
return () => controller.abort(); // ⚠️ cancel on unmount / url change to avoid races
}, [url]);
return state;
}
🟢 In production prefer TanStack Query / SWR over hand-rolled useFetch—they add caching, dedup, retries, and revalidation. useFetch is great for interviews and small apps.
Composition pattern 🟢#
Custom hooks compose—build small hooks and combine them into task-specific ones. This keeps each hook single-purpose and testable.
function useDebouncedSearch(query) {
const debounced = useDebounce(query, 400); // compose useDebounce
return useFetch(`/api/search?q=${debounced}`); // …with useFetch
}
⚠️ Return stable references where consumers may put them in dependency arrays—wrap returned callbacks in useCallback and derived objects in useMemo, or the hook's output changes every render and defeats downstream memoization.
Interview Q&A#
Q1. What makes something a custom hook rather than a regular function?
It calls React hooks and its name starts with use, which lets React and the linter enforce the Rules of Hooks. A plain helper that calls no hooks doesn't need the prefix.
Q2. If two components use the same custom hook, do they share state? No. A custom hook shares logic, not state—each call creates its own independent state, effects, and refs. They're as isolated as if you inlined the code.
Q3. Why must a hook's name start with use?
So React's linting (Rules of Hooks) can verify hooks are called unconditionally at the top level. The convention is how tooling distinguishes hook calls from ordinary function calls.
Q4. Why wrap returned callbacks from a custom hook in useCallback?
Consumers often place returned values in dependency arrays or pass them to React.memo children. Stable references prevent unnecessary effect re-runs and re-renders downstream.
Q5. How did custom hooks replace HOCs and render props? They share stateful logic without adding wrapper components to the tree, avoiding "wrapper hell" and prop-name collisions. Logic composes as plain function calls instead of nested components.