Skip to content

Part 7.3 — Custom Hooks as Architecture: Patterns & Pitfalls

Site Console Site Console
3 min read Updated Feb 3, 2026 Frontend Design 0 comments

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:

  • useUsers

  • useAuth

  • usePagination

Bad:

  • useAppData

  • useEverything

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

Leave a comment

Sign in to leave a comment.

Comments