skip to content

A shared useDashboard() hook in your codebase takes an options object with eight boolean flags and returns fifteen fields, and every team is now afraid to change it. How do you decide where to split it, and what do you weigh when drawing hook boundaries in a shared library?

level: principalimportance: nice to knowfreq 30%

answer

  1. flags mean several hooks fused
  2. one source of truth per hook
  3. group returned fields by lifetime
  4. composition runs everything, always
  5. keep the old name as a shim

basics

~20 s

Split by concern and lifetime, not by size: each hook should own one source of truth or one subscription. Behaviour-selecting flags are the strongest signal that several hooks were fused, so extract those first and keep a thin composite for existing callers.

solid answer

~50 s

I start from the flags, because a boolean that switches behaviour is almost always two hooks welded together — the callers passing `{ withCharts: false }` are paying for and depending on code they never use. So I inventory the call sites, group the returned fields by which flag and which data source they belong to, and each coherent group becomes its own hook that owns one source of truth or one subscription. Then I keep `useDashboard()` as a thin composite that calls the small hooks and re-exports their results, so nothing has to migrate on day one, and move call sites to the specific hooks as they are touched. What I weigh: how much unnecessary work each caller does, the blast radius of a change (a hook fifty components call is API surface with an owner and tests), how testable each piece is alone, and the real cost of composition — a composite runs every hook inside it, unconditionally, for every caller.

go deeper

for a junior

Know that hooks can call other hooks, so a large hook can be broken into smaller ones — and that a hook doing several unrelated jobs is a sign it should be split.

for a middle

Explain the mechanics: extract each concern into its own hook, have the original call them and re-export the results, and note that hook calls must stay unconditional so flags cannot simply skip a hook.

for a senior

Show the method — audit call sites, cluster the returned fields by data source and lifetime, delete dead fields, then extract — and be explicit that every hook in a composite runs and re-renders for every caller.

for a principal

Own the layering and the rollout: who owns a hook fifty components call, how the contract is tested, how the split ships incrementally behind a thin shim, and what convention stops shared hooks from re-accumulating flags.

## Why the flags are the diagnosis An options object of boolean flags is the clearest structural smell a hook can have. Each flag means "run a different subset of this hook", and since hooks cannot be called conditionally, the flag usually gates work *inside* the hook while the hook calls still all execute. Callers therefore pay for machinery they disabled, and worse, the hook's behaviour is now a matrix: eight booleans is 256 nominal configurations, of which the team has actually exercised maybe five. Nobody can reason about — or test — the rest, which is precisely why everyone is afraid to change it. The reframe is to stop asking "how do I make this hook configurable?" and ask "what are the distinct concerns here?" Flags that select behaviour become separate hooks; only parameters that *tune* one behaviour (an id, a page size, a debounce interval) deserve to stay options. ## Where the seams are Two criteria draw good boundaries: **One source of truth per hook.** A hook should own one piece of state, one subscription, or one derived view of one thing. If the returned fields split cleanly into groups that never read each other, those are separate hooks. Fields that must change together — a value and its updater, a request's data and its error — belong in the same hook. **One lifetime per hook.** State that lives for the mount, a subscription torn down on unmount, and a value that resets per route change have different lifecycles. Mixing them makes cleanup logic conditional and hard to reason about; separating them makes every cleanup obvious. A practical way to find the seams is to map the fifteen returned fields against the call sites: which components use which fields, and which flags they pass. Clusters fall out of that table, and the fields nobody uses get deleted rather than redesigned. ## Composition and its real cost Hooks compose by ordinary function calls, and there is no limit to how deeply. That makes it tempting to keep a convenience hook that calls all the small ones, and there is a place for it — a screen-level hook that assembles what one page needs is a legitimate abstraction, and it is the migration path that lets you split without a big-bang refactor. But composition is unconditional. Every hook inside a composite runs for every caller, sets up its subscriptions, fires its effects and triggers a re-render when any of them updates. A component that needs one of five concerns re-renders when any of the other four change. So the rule is: compose at the *usage* layer, close to the screen that genuinely needs everything, and let the general-purpose layer stay small and independent. What you must not do is push the composite down into the shared library as the only supported entry point — that recreates the same problem behind a nicer name. ## The organisational weights At this scale the decision is not purely technical: - **Blast radius.** A hook called by fifty components is API surface. It needs a named owner, tests that pin its contract (including which callbacks are identity-stable), and a change process. Splitting it is partly about shrinking the number of teams a change can break. - **Migration path.** Keep the old name as a thin wrapper over the new hooks and move call sites opportunistically as files are touched. A required same-day migration across teams is how splits get abandoned halfway, leaving both shapes alive. - **Deletion.** The audit almost always finds returned fields with no consumers and flags with one caller. Removing them is cheaper than designing around them, and it is the part of the work with no downside. - **Ownership boundaries.** A hook that mixes one team's domain data with another's UI concerns will be edited by both and understood by neither. Boundaries that follow ownership survive. - **Layering.** The healthy shape is a thin layer of primitives (one subscription each), a domain layer that composes them into meaningful concepts, and per-screen composites that nobody else imports. Fear of change concentrates wherever those layers are collapsed into one function. ## How to answer Name the flag smell as the diagnosis, give a concrete method (inventory call sites, cluster returned fields by source and lifetime, delete the unused, extract clusters, keep a thin composite as the migration shim), then show that you know composition is not free and that the deciding factors at this scale are blast radius, ownership, and having a migration path that does not require a flag day.

  • When is a convenience hook that composes five smaller hooks still the right thing to ship?
    When it lives at the screen layer and every caller genuinely needs all five — a page-level hook that assembles exactly what one route renders is a real abstraction and keeps the component readable. The failure mode is publishing it as the shared library's main entry point, so unrelated consumers inherit five subscriptions to get one value.
  • How do you decide which parameters stay as options and which become separate hooks?
    Parameters that tune one behaviour — an id, a page size, a debounce interval — stay. Booleans that switch whole blocks of behaviour on and off mean two behaviours are sharing one function, and they should become two hooks. A quick test: if flipping the flag changes which fields of the return value are meaningful, it is a seam, not an option.
  • What would you put in place so the split hooks do not re-converge into a god hook a year later?
    An explicit layering rule with an owner: primitives own one source each, domain hooks compose them, screen composites are not exported from the shared package. Back it with review guidance that a new boolean flag on a shared hook needs justification, and with contract tests per hook so splitting stays cheaper than bolting on.

saying these in an interview costs you the question

  • Adds a ninth flag instead of questioning the eight
  • Says composition is free because hooks are just function calls
  • Plans a big-bang migration of every call site at once
  • Splits by line count rather than by concern or lifetime
  • Keeps unused returned fields because someone might need them

context