Custom React Hooks: When to Extract and When to Leave It Inline

Technical PM + Software Engineer
Most teams don’t struggle because they write too few hooks. They struggle because they extract the wrong logic into hooks at the wrong time.
A custom hook should reduce cognitive load, not just reduce lines of code.
This guide gives you a practical decision framework for when extraction improves architecture and when it creates indirection that slows delivery.
The core mistake: line-count optimization
A common anti-pattern:
- component feels long,
- logic gets moved into
useSomething, - readability gets worse because behavior is now split across files with vague naming.
A shorter component is not automatically a better component.
The question is:
Does extraction create a clearer behavior boundary that multiple consumers can use correctly?
When extraction is a good idea
Extract to a hook when all are true:
- The behavior is coherent (one clear responsibility).
- The behavior is reused or expected to be reused soon.
- The hook API can stay small and intention-revealing.
- The behavior changes independently from presentation.
Examples that usually belong in hooks:
- debounced search lifecycle,
- intersection observer state,
- keyboard shortcut handling,
- async resource loading with cancellation.
When to keep logic inline
Keep logic inline when:
- the behavior is highly local and still evolving,
- extraction would require too many parameters,
- naming is vague (
useData,useUtils,usePageStuff), - the hook would return a large kitchen-sink object.
In plain English: if you cannot name the hook by behavior in one clear sentence, you probably should not extract it yet.
Good extraction example: debounced query state
type UseDebouncedQueryOptions<T> = {
query: string;
enabled?: boolean;
delayMs?: number;
fetcher: (query: string, signal: AbortSignal) => Promise<T[]>;
};
export function useDebouncedQuery<T>({
query,
enabled = true,
delayMs = 300,
fetcher,
}: UseDebouncedQueryOptions<T>) {
const [data, setData] = React.useState<T[]>([]);
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<Error | null>(null);
React.useEffect(() => {
if (!enabled || !query.trim()) {
setData([]);
setError(null);
return;
}
const ctrl = new AbortController();
const timer = setTimeout(async () => {
try {
setLoading(true);
setError(null);
const next = await fetcher(query, ctrl.signal);
setData(next);
} catch (e) {
if (!(e instanceof DOMException && e.name === "AbortError")) {
setError(e as Error);
}
} finally {
setLoading(false);
}
}, delayMs);
return () => {
ctrl.abort();
clearTimeout(timer);
};
}, [query, enabled, delayMs, fetcher]);
return { data, loading, error };
}
This extraction works because it captures one behavior boundary cleanly.
Bad extraction example: the “god hook”
Smell profile:
- performs fetch + validation + analytics + routing + modal orchestration,
- takes many unrelated parameters,
- exposes many unrelated values.
This is not reuse. It is hidden complexity.
Fix:
- split into focused hooks or keep local logic inline until boundaries stabilize.
Hook API design rules that scale
Before implementation, design the API:
- name by behavior (
useDisclosure,useDebouncedQuery,useKeyboardNav), - keep inputs explicit,
- return only what consumers need,
- avoid leaking implementation details.
Bad:
const stuff = useFeatureEngine(config);
Better:
const { open, openPanel, closePanel } = useDisclosure(false);
Hook quality checks before shipping
Use this in PR review:
- Can purpose be explained in one sentence?
- Is responsibility singular?
- Is API smaller than logic it replaced?
- Are side effects explicit and predictable?
- Are dependencies correctly modeled?
- Would a new engineer use this correctly without reading internals?
If multiple answers are “no,” request inline refactor or narrower extraction.
Testing strategy
Test at two layers:
- Hook behavior tests: transitions, retries, cancellation, edge cases.
- Consumer integration tests: verify real component usage contract.
Keep domain logic in pure helpers where possible, then test helpers with fast unit tests and hook orchestration separately.
Migration strategy: introducing hooks safely
- Identify one repeated behavior in two real components.
- Extract minimal version.
- Migrate one consumer first.
- Validate readability + bug rate.
- Migrate remaining consumers after API stabilizes.
Do not pre-generalize for hypothetical future screens.
Common anti-patterns
- Extracting for style consistency instead of behavior reuse.
- Hook names that describe implementation, not intent.
- Hooks with broad option bags and hidden defaults.
- Constantly fighting dependency arrays due to poor state boundaries.
Closing
Custom hooks are high leverage when they package stable behavior into a narrow, clear API.
They become drag when used as a hiding place for complexity.
Use extraction as an architectural decision, not a formatting habit. If intent gets clearer and change gets safer, extract. If not, keep it inline and refactor locally first.