Volume 9

Q377: Refactoring Drill Extracting Reusable Hooks Without Overengineering

Difficulty: SeniorFrequency: HighAnswer time: 20-25 minutes

What Interviewers Want To Evaluate

This drill tests whether you can improve React code without creating unnecessary abstractions.

Senior frontend engineers refactor for clarity, reuse, correctness, and ownership.

They do not extract hooks just because two files look similar.

The interviewer wants to see whether you can identify real duplication, preserve behavior, and leave the code easier to reason about.

Short Interview Answer

I extract a reusable hook only when there is repeated stateful behavior with the same lifecycle, inputs, outputs, and failure rules. Before extraction, I name the behavior, write or identify tests, and make sure the hook has a small API. I avoid mixing UI rendering, domain decisions, and generic utilities inside one hook because that makes reuse fragile.

Refactoring Trigger

A hook extraction is worth considering when duplication includes:

same state transitions
same effect lifecycle
same cleanup behavior
same async cancellation rule
same validation logic
same event subscription
same persistence boundary

It is weaker when duplication is only:

same variable names
similar JSX shape
similar button layout
similar API endpoint pattern
two files written by the same person

Behavioral duplication matters more than visual similarity.

Example Before

Imagine two components both manage a debounced value:

function ProductSearch() {
  const [query, setQuery] = useState("");
  const [debouncedQuery, setDebouncedQuery] = useState("");

  useEffect(() => {
    const id = window.setTimeout(() => setDebouncedQuery(query), 300);
    return () => window.clearTimeout(id);
  }, [query]);

  return <ProductResults query={debouncedQuery} onQueryChange={setQuery} />;
}

The same effect appears in multiple places.

This is a good extraction candidate because the lifecycle is identical.

Extracted Hook

A focused hook:

function useDebouncedValue<T>(value: T, delayMs: number) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timeoutId = window.setTimeout(() => {
      setDebouncedValue(value);
    }, delayMs);

    return () => window.clearTimeout(timeoutId);
  }, [value, delayMs]);

  return debouncedValue;
}

This hook is small.

It has one responsibility.

It does not know about products, users, routing, analytics, or API calls.

Hook API Design

Keep hook APIs boring:

primitive inputs where possible
clear return values
stable behavior
few options
no hidden global state
no surprising side effects

Avoid option objects too early.

An option object is useful when there are several optional behaviors.

For one or two required values, explicit parameters are often clearer.

Overengineering Warning

This is suspicious:

useSearchOrPickerOrFilterState({
  mode,
  syncUrl,
  persist,
  debounce,
  analytics,
  permissions,
  initialQuery,
  resultMapper,
});

That hook is trying to own multiple product concepts.

It may become harder to test than the original components.

Senior refactoring should reduce complexity, not hide it.

Boundaries To Preserve

Separate:

generic stateful behavior
domain rules
data fetching
UI rendering
analytics
permissions
route synchronization

Some hooks can compose other hooks.

But each layer should have a reason.

If everything is inside one hook, the abstraction becomes a private framework.

Testing Strategy

Before extraction:

identify expected behavior
cover edge cases
protect cleanup behavior
verify existing component behavior

After extraction:

test hook behavior if reusable
test component integration
test one representative consuming workflow

Do not rely only on TypeScript.

Lifecycle behavior needs runtime tests.

Refactoring Steps

Use this sequence:

name the behavior
write or identify tests
extract the smallest hook
replace one usage
verify behavior
replace other usages
delete duplication
review hook API
document only if needed

Replacing one usage first lowers risk.

It also reveals whether the hook API is awkward.

Interview Follow-Up

If asked whether to extract immediately, say:

I would first check whether this duplication is stable.
If only two places share similar code and the product behavior is still changing, I may keep it local.
If three or more places share the same lifecycle and bug fixes would need to be repeated, I would extract a hook.

This answer shows restraint.

Senior engineers understand that wrong abstractions are expensive.

Good Hook Examples

Reasonable hooks:

useDebouncedValue
useLatest
useMediaQuery
useLocalStorageState
useAbortableRequest
useFocusTrap
usePreviousValue

These describe behavior.

They are easier to reuse than hooks named after one screen.

Bad Hook Smells

Watch for:

hook name includes multiple domains
too many options
returns a large object
performs navigation unexpectedly
fires analytics automatically
depends on hidden module state
requires consumers to know internal timing

These signs mean the abstraction may need to be split.

Common Mistakes

  • Extracting after only seeing surface similarity.
  • Mixing UI JSX into reusable behavior hooks.
  • Creating a hook with too many responsibilities.
  • Forgetting cleanup behavior.
  • Making dependencies unstable.
  • Losing tests during refactor.
  • Creating a generic abstraction that only one screen understands.

Final Mental Model

A good hook extraction is:

repeated behavior
clear lifecycle
small API
testable contract
low surprise
easy deletion

Refactoring is successful when future readers understand the code faster.