Concept
The beginner framing: a custom hook lets you extract reusable stateful logic, like "subscribe to window size" or "debounce a value", into a function you can call from multiple components, instead of copy-pasting the same useState/useEffect combination everywhere.
The precise mental model: a custom hook is nothing more than a regular JavaScript function that calls one or more other hooks internally. There's no special React API for "declaring" a hook, you're not registering it anywhere. React can't even tell the difference between a custom hook and a component at the language level; the only thing that matters is that whatever hooks it calls still obey the same Rules of Hooks (see Hooks Overview), called unconditionally, at the top level, every time the custom hook itself runs.
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
return width;
}
function Sidebar() {
const width = useWindowWidth(); // just a function call
return <aside>{width < 600 ? "Collapsed" : "Expanded"}</aside>;
}useWindowWidth composes useState and useEffect into one reusable piece of behavior, every component that calls it gets its own independent width state and its own independent subscription, exactly as if each had written the useState/useEffect pair inline.
The use prefix is a convention, not a language rule
function useSomething() { /* calls hooks inside, MUST follow Rules of Hooks */ }
function getSomething() { /* calls hooks inside, WOULD be a lint violation */ }Nothing in JavaScript or React enforces the use prefix, you could name the function anything and it would run identically. The prefix exists so eslint-plugin-react-hooks (and human readers) know to apply the Rules-of-Hooks checks to that function's body. Skipping the prefix doesn't break anything at runtime, but it silently disables the linting safety net that would otherwise catch a Rules-of-Hooks violation inside it.
Try It
Predict what happens before checking the solution.
function useToggle(initial) {
const [value, setValue] = useState(initial);
const toggle = () => setValue((v) => !v);
return [value, toggle];
}
function Panel() {
const [isOpenA, toggleA] = useToggle(false);
const [isOpenB, toggleB] = useToggle(true);
return /* ... */;
}Does calling the same custom hook twice in one component cause any conflict between isOpenA and isOpenB?
Solution
No conflict, each call to useToggle gets its own, independent slot(s) in Panel's hook list. The first useToggle(false) call occupies whatever slot index it's called at (say, slot 0), and the second useToggle(true) call occupies the next slot (slot 1), exactly the same as if Panel had called useState twice directly. A custom hook doesn't introduce a separate, isolated slot list; it just calls hooks that get slotted into the caller's ongoing sequence, in the order the custom hook itself calls them.
Implement It Yourself
Build a useDebounce hook, one of the most common real custom hooks, and a frequent live-coding interview exercise:
function useDebounce(value, delayMs) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timeoutId = setTimeout(() => {
setDebounced(value); // only commits after `delayMs` of no further changes
}, delayMs);
return () => clearTimeout(timeoutId); // a NEW value arrived before the timer fired, cancel it
}, [value, delayMs]);
return debounced;
}
// Usage: only fires a search request 300ms after the user stops typing
function SearchBox() {
const [query, setQuery
Every keystroke changes value, which changes the effect's dependency array, which cancels the previous timer (via cleanup) and starts a new one, debounced only ever catches up to value once typing pauses for delayMs. This is the exact composition pattern every custom hook follows: combine existing hooks, expose only the final value/API the caller needs.
Under the Hood
Composing simple hooks into a more powerful custom hook is the same idea as composing simple pure functions in Functional Programming, useDebounce is built from useState and useEffect exactly the way a function like pipe(parse, validate, transform) is built from smaller functions, each doing one job. And because a custom hook's internal hook calls still slot into the calling component's ordinary hook list (see Hooks Overview), there's no additional runtime mechanism to learn here, it's the same slot-list model, just with the composition hidden behind a named function.
Common Mistakes
1. Forgetting the hooks inside a custom hook still obey the Rules of Hooks
function useUser(id) {
if (!id) return null; // ❌ early return BEFORE a hook call below it
const [user, setUser] = useState(null);
// ...
}The custom hook itself is just a function, but any hook calls inside it are still subject to the exact same top-level, unconditional, every-render requirement as if they were written directly in a component.
2. Returning an inconsistent shape across renders
function useFeature(flag) {
if (flag) return { enabled: true, data: fetchData() };
return null; // ❌ sometimes an object, sometimes null, awkward for every caller
}Prefer a consistent return shape (an object with stable keys, or a fixed-length tuple like [value, setValue]) so every call site can destructure it the same way, regardless of the hook's internal state, mirroring how useState always returns a 2-element array, never sometimes just a bare value.
3. Treating a custom hook as a way to share STATE instances between components
const useSharedCounter = () => useState(0); // ❌ each caller gets its OWN independent stateA custom hook is a reusable piece of logic, not a reusable piece of state, every component that calls useSharedCounter gets its own separate count, completely independent of every other caller's. To genuinely share one value across multiple components, that state needs to live in a common ancestor and be passed down, or live in Context (see Context API).
Best Practices
- Extract a custom hook once the same stateful logic appears in two or more components, not preemptively for a single use site.
- Return the smallest, most stable API the caller actually needs, a value and a setter, or a value and an action function, rather than exposing every internal piece of state.
- Keep a custom hook focused on one concern (
useDebounce,useLocalStorage,useWindowWidth) rather than one hook trying to manage several unrelated pieces of behavior. - Document the hook's dependencies and cleanup behavior in its name or a short comment, a hook that subscribes to something should make it obvious that it also unsubscribes.
Performance Tips
- A custom hook adds no runtime overhead beyond the hooks it calls internally, there's no extra "wrapper" cost. Profile the underlying
useState/useEffect/useMemocalls it's built from, not the custom hook abstraction itself. - If a custom hook returns a new object or array literal on every call (
return { value, setValue }reconstructed fresh each time), that new reference can defeatReact.memoin any component receiving it as a prop, wrap the returned value inuseMemoif downstream memoization depends on referential stability.
