Reusing logic with custom hooks - discussion (2022-11-19)
After reading this again, I’m realizing most of our hook problems aren’t “hooks are bad”, it’s that hook surfaces are unstable and hide important work (effects, fetches, identity churn).
What rules do you use to keep custom hooks small and owned (inputs, outputs, stability)? Do you treat hook output shapes as contracts (and render a small debug marker when it matters), or is that overkill? How do you stop hooks from turning into mini-frameworks as teams pile on features?
Comments (10)
Back to latestIf a hook returns 12 things, it’s not reuse, it’s a module with a use prefix.
Treat hook surfaces like APIs: narrow surface, stable identities, and no hidden fetches.
When something is part of the render contract, make it route-visible instead of burying it in a hook.
Related: API Reference and Performance and Rendering Best Practices.
Stability trick: return memoized surfaces so consumers don’t rerender by accident:
tsx
export function useFiltersSurface() {
const [q, setQ] = useState('');
const actions = useMemo(() => ({ setQ }), [setQ]);
const state = useMemo(() => ({ q }), [q]);
return useMemo(() => ({ state, actions }), [state, actions]);
}Counterpoint: I’d rather keep logic inline longer and only extract once the surface stabilizes.
Hooks are “cheap” to create, which is why they get abused.
That’s reasonable. Our failure mode is the opposite: people keep bolting features onto the first hook that looks close enough.
So the hook becomes the dumping ground and nobody can reason about the contract.
Yep. Extracting later doesn’t help if you never stop adding responsibilities.
We banned hooks that perform network calls and return { data, loading, error } as a pattern.
If data is a contract, it belongs at the route boundary with explicit cache posture.
The “render a small debug marker” suggestion sounds silly until you’ve had to debug a hook from a screenshot.
We render data-hook-surface='filters:v1' in dev builds and it’s saved time.
Hidden effects inside hooks caused our nastiest timing bugs.
Once we required “effect markers” for hook-owned effects, the quality jumped because reviewers could see what the hook actually does.
Naming is surprisingly important. A vague hook name correlates strongly with a vague hook contract.
If you can’t name what it owns, it probably owns too much.
A hook you can’t test without mocking half the app is a hook that’s doing too much.
Guardrail we used: hooks can’t write to global-ish state without an owner + reason string.
It felt annoying at first, then it prevented a bunch of accidental coupling.