Part 7.3 — Custom Hooks as Architecture: Patterns & Pitfalls
Custom hooks are one of React’s most powerful ideas—and one of the easiest to misuse.
They let you extract logic, share behavior, and compose features. But they also let you hide too much, too early, in the wrong place. This post shows how to treat custom hooks as architectural tools, not just refactoring tricks.
1. What a Custom Hook Really Is
A custom hook is not a reusable component.
It’s a reusable unit of behavior.
Good hooks:
encapsulate a single responsibility
expose a clear, minimal API
make components smaller and more readable
Bad hooks:
mix unrelated concerns
hide control flow
make behavior implicit
2. When You Should Extract a Hook
Extract a hook when:
logic is reused across components
a component is doing too many things
side effects clutter rendering logic
state transitions deserve a name
Example smell:
useEffect(() => {
setLoading(true);
fetch(`/api/users?page=${page}`)
.then(res => res.json())
.then(setUsers)
.finally(() => setLoading(false));
}, [page]);This logic wants a boundary.
3. A Focused Data-Fetching Hook
type UseUsersResult = {
users: User[];
status: 'idle' | 'loading' | 'error';
};
export function useUsers(page: number): UseUsersResult {
const [users, setUsers] = React.useState<User[]>([]);
const [status, setStatus] =
React.useState<'idle' | 'loading' | 'error'>('idle');
React.useEffect(() => {
setStatus('loading');
fetch(`/api/users?page=${page}`)
.then(res => res.json())
.then(data => {
setUsers(data);
setStatus('idle');
})
.catch(() => setStatus('error'));
}, [page]);
return { users, status };
}The hook:
owns the side effect
exposes state declaratively
keeps the component clean
4. Hooks Should Not Decide UX
Bad hook API:
return { users, loading, error, showToast };Hooks should not:
show notifications
navigate routes
manipulate the DOM
Those are UI concerns.
Good hook APIs return:
data
status
functions
Components decide how to react.
5. Hooks as Contracts
Think of a hook’s return value as a contract.
const { users, status } = useUsers(page);This tells you:
what data exists
what states are possible
If consumers need to “guess” behavior, the hook API is wrong.
6. Avoid “Mega Hooks”
A common anti-pattern:
useDashboard();Which returns:
user
permissions
metrics
notifications
feature flags
This creates:
tight coupling
hidden dependencies
painful refactors
Prefer small, composable hooks:
useAuth();
useMetrics();
useNotifications();Composition beats consolidation.
7. Composing Hooks Deliberately
Hooks compose naturally:
function useDashboardData() {
const auth = useAuth();
const metrics = useMetrics(auth.user?.id);
return { auth, metrics };
}Composition should:
remain explicit
avoid circular dependencies
keep data flow obvious
If composition hides too much, stop and split.
8. Hooks and Dependencies: Be Explicit
Always treat dependencies seriously.
useEffect(() => {
load();
}, [page]);Avoid:
disabling lint rules
“it works” dependency arrays
Hidden dependencies create bugs that surface months later.
9. Hooks vs Context: Know the Boundary
Hooks are for behavior.
Context is for distribution.
Bad:
useSomethingGlobal();That secretly depends on context.
Good:
const value = useSomething();With context usage explicit at the provider level.
Hooks should not silently reach into global state unless that dependency is obvious and documented.
10. Testing Custom Hooks
Test hooks through behavior.
function TestComponent() {
const { users, status } = useUsers(1);
if (status === 'loading') return <span>Loading</span>;
return <span>{users.length}</span>;
}Render and assert behavior.
Avoid testing hook internals directly.
11. Common Hook Smells
Watch for:
hooks that return many unrelated values
hooks that mutate global state
hooks that perform navigation
hooks that require many flags to “configure”
These indicate missing architectural boundaries.
12. Hooks as a Design Tool
Well-designed hooks:
clarify intent
reduce duplication
enforce consistency
make refactors safer
Poorly designed hooks:
hide control flow
couple distant features
create invisible dependencies
Treat hook APIs with the same care as public libraries.
13. A Simple Rule That Works
If a hook’s name starts to feel vague, it’s probably doing too much.
Good:
useUsersuseAuthusePagination
Bad:
useAppDatauseEverything
Names reveal design quality.
14. Summary
You now know how to:
treat custom hooks as architectural seams
extract hooks for behavior, not UI
design clear hook APIs
compose hooks safely
avoid over-abstraction
recognize hook-related smells
Related
Part 7.5 — Build Optimization with Vite: Code Splitting & Env Handling
Vite makes development fast by default. This post shows how to make production builds just as intentional—lean bundles, predictable envs, and code that ships only when needed.
Part 7.4 — Performance Hooks: useMemo, useCallback & useDeferredValue
Performance hooks are scalpels, not band-aids. This post teaches you how to use them intentionally—only where they solve real problems.
Part 7.2 — Advanced Routing Patterns: Protected Routes & URL State
Routing becomes powerful when URLs express intent. This post shows how to protect routes, manage auth flows, and encode UI state in the URL—without hacks.
Comments