React Forum
Blog & Guide Responses
Jan 20, 2023 · 04:00 AM

Reusing logic with custom hooks - discussion (2023-01-20)

Started by Theo Bennett
externalreact.devcustom hooksreuseeffectscontractsevidence

This page is often used as justification for extracting logic into hooks, but I still see hooks become invisible state machines (hidden effects, hidden writes, hidden assumptions). I'm curious what rules people use to keep hooks honest as a contract surface instead of a grab bag.

Read the article

What do you allow custom hooks to do (effects, writes, subscriptions), and what do you forbid? When do you stop extracting hooks and move behavior into a component boundary for legibility? How do you test hooks that encode motion without snapshotting timing?

Comments (18)

Back to latest
Marisol Vega
Jan 24, 2023 · 09:12 AM

Our rule: hooks can compute and format, but state writes are explicit callbacks passed in.

If a hook writes internally, it needs a really clear contract and evidence output.

Dmitri Kline
Feb 03, 2023 · 06:33 PM

A small example of a read-only hook that stays honest:

ts
export function useVisibleIds(items: Array<{ id: string; title: string }>, q: string) {
  const query = q.trim().toLowerCase();
  return query ? items.filter((x) => x.title.toLowerCase().includes(query)).map((x) => x.id) : items.map((x) => x.id);
}

Then the caller chooses whether to store derived ids or recompute.

Keira Santos
Feb 18, 2023 · 05:02 AM

Counterpoint: some hooks should write state, otherwise you just end up duplicating wiring everywhere.

But the hook should return a status lane so behavior is observable, not just "it does stuff".

Theo Bennett
Feb 21, 2023 · 08:14 PM

Agree. Hidden writes are the problem, not writes per se.

If the hook returns a lane + evidence, it becomes reviewable.

Grace McCoy
Mar 05, 2023 · 12:30 PM

We prefer component boundaries when the behavior has lifecycle and UI implications (loading, errors, retries).

Hooks are better when they're purely calculation or event glue.

Jasper Nguyen
Mar 23, 2023 · 06:44 AM

Testing hooks got easier once we stopped asserting on timing and started asserting on evidence outputs (status flags, derived values).

If the hook doesn't expose evidence, it probably isn't reusable.

Nina Kapoor
Apr 09, 2023 · 07:01 PM

We also avoid hooks that implicitly read route state/context because it hides dependencies.

If a hook depends on route posture, pass the posture key in explicitly.

Camille Duarte
Apr 27, 2023 · 02:22 AM

A pattern we use for hooks with effects: the hook returns a debug string that the caller can render as evidence.

It keeps behavior visible without leaking implementation details.

Owen Price
May 12, 2023 · 08:09 AM

Counterpoint: too much hook extraction can be premature; you need the surface to stabilize first.

We try to inline the first version in a component, then extract once the interface is obvious.

Marisol Vega
May 16, 2023 · 05:03 PM

Same. If you extract too early, you freeze the wrong abstraction and it becomes painful to change later.

Hooks should be earned, not assumed.

Benji Rios
Jun 03, 2023 · 09:21 PM

Docs tie-in: hook extraction interacts with state discipline.

If siblings need consistent derived keys, extracting a hook that re-derives them in each consumer can cause drift.

Related: State Management (Best Practices) and Components and Composition.

Lina Ortega
Jun 18, 2023 · 04:18 AM

A practical contract we use in code review: hooks declare reads/writes/effects in the PR description.

It's boring but it prevents "spooky" hooks.

Amina Farouk
Jul 06, 2023 · 01:55 PM

We also limit hook returns: return a small object with named keys, not a giant tuple.

If the surface is named, it stays readable.

Tomas Rivera
Jul 23, 2023 · 09:05 AM

Code snippet: a hook that returns a lane (observable) while keeping writes explicit:

ts
type Lane = 'idle' | 'pending' | 'error' | 'ok';

export function useSubmitLane() {
  const [lane, setLane] = useState<Lane>('idle');
  const submit = async (fn: () => Promise<void>) => {
    setLane('pending');
    try { await fn(); setLane('ok'); } catch { setLane('error'); }
  };
  return { lane, submit };
}
Sofia Valdez
Aug 11, 2023 · 10:10 PM

We found a lot of hooks should really be small modules (pure functions).

If there's no React lifecycle, it doesn't need to be a hook.

Priya Liao
Sep 01, 2023 · 03:03 AM

A misunderstanding I see: hooks are treated as "free reuse" but they can hide coupling.

Passing inputs explicitly and returning evidence makes the coupling visible.

Evan Brooks
Sep 22, 2023 · 12:12 PM

Counterpoint: sometimes deep props are simpler than inventing hooks/context.

Inside a single route shell, prop threading can be the clearest contract.

Grace McCoy
Sep 25, 2023 · 09:18 PM

Agreed. The more "global" you make a hook, the more careful you need to be about ownership and evidence.

Props are honest about dependencies.

Salma Qureshi
Oct 18, 2023 · 05:55 AM

If you want hooks to stay healthy, require two things: small surface area and observable behavior.

Everything else is negotiable.

Hana Saito
Nov 09, 2023 · 02:22 PM

The best custom hooks in our codebase read like utilities with a small state lane attached. The worst ones are mini frameworks.

Benji Rios
Dec 03, 2023 · 07:20 AM

We started tagging hooks in our docs by whether they run effects.

That alone made people more careful about where they used them.