Custom Hooks
IntermediateA custom hook extracts stateful logic so it can be reused and tested. It shares behaviour, never state — every caller gets its own copy.
Overview
Custom hooks are the answer to a question that once needed render props and higher-order components: how do two unrelated components share logic that uses state or effects? A hook is just a function calling other hooks, named with a use prefix so the lint rules can check it. The point most people miss on the first pass is that hooks share code, not values — calling useCounter in two components creates two independent counters. Shared state needs context or a store.
Extracting One
Take the state and effects out of the component; leave the markup behind.
function useDebouncedValue(value, delay = 300) {
const [debounced, setDebounced] = useState(value)
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay)
return () => clearTimeout(id)
}, [value, delay])
return debounced
}
function useLocalStorage(key, initial) {
const [value, setValue] = useState(() => {
try { return JSON.parse(localStorage.getItem(key)) ?? initial }
catch { return initial }
})
useEffect(() => {
try { localStorage.setItem(key, JSON.stringify(value)) } catch {}
}, [key, value])
return [value, setValue]
}
// Return an array for a two-value tuple people will rename,
// an object when there are several named things:
return { data, isLoading, error, refetch }The Rules
Two rules, one reason: hooks are matched by call order.
// 1. Only call hooks at the TOP LEVEL — never in a condition,
// a loop, or after an early return
// 2. Only call hooks from a component or another hook
function Bad({ isPro }) {
if (isPro) {
const [x, setX] = useState(0) // call order changes between renders
}
// React tracks hooks by position, so on the render where isPro
// flips, every subsequent hook receives the wrong state.
}
function Good({ isPro }) {
const [x, setX] = useState(0) // always called
if (!isPro) return <Upgrade /> // early return AFTER the hooks
}
// The eslint plugin enforces both — never disable it.
// A hook may itself be conditional in what it DOES, just not in
// whether it is called:
useQuery({ queryKey: [...], enabled: isPro })When to Extract, and What It Does Not Do
The signals for extraction, and the misconception about shared state.
// Extract when:
// - the same state + effect pattern appears twice
// - a component's logic is long enough to hide the markup
// - the logic deserves its own test
// - it wraps a browser API (media query, online status, geolocation)
// Do not extract a hook that only wraps one useState — that is
// indirection with no payoff.
// The misconception: hooks do NOT share state.
function A() { const [n, inc] = useCounter() } // A's counter
function B() { const [n, inc] = useCounter() } // B's own, separate counter
// Same logic, separate instances. For one shared value, put the
// hook's state in a context provider, or use a store.
// Composition is the real strength — hooks calling hooks:
function useProblemPage(slug) {
const { data: problem, isLoading } = useProblem(slug)
const { user } = useAuth()
const locked = problem?.isPro && !user?.isPro
useDocumentTitle(problem?.title)
return { problem, isLoading, locked }
}Key Points to Remember
- 1A custom hook is a function starting with "use" that calls other hooks — that prefix is what the lint rules key on
- 2Hooks are matched by call order, so they must never be called conditionally or inside a loop
- 3Early returns go after all hook calls, never between them
- 4Hooks share logic, not state — two callers get two independent copies
- 5Extract when the pattern repeats or deserves its own test; wrapping a single useState is not worth the indirection
Interview Questions
Sign in to ask AriaWhy can hooks not be called conditionally?
If two components use the same custom hook, do they share state?
When is extracting a custom hook not worth it?
Ask Aria about Custom Hooks
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.