Writing custom hooks that survive production
The short answer
A good custom hook has a name starting with "use", returns either a tuple (for one or two values) or an object (for three or more), keeps every callback stable with useCallback or a ref, cleans up every subscription it creates, guards browser APIs for server rendering, and never accepts a config object it recreates internally. Extract a hook when the same stateful logic appears in a second component — not before.
A custom hook is just a function that calls other hooks. That simplicity is the point — and the reason a bad one spreads through a codebase before anyone notices.
1. Extract on the second use, not the first
Premature abstraction is worse than duplication. The first time you write the logic you do not yet know which parts vary. The second time you do, and the right shape becomes obvious.
2. Name it for what it gives you
The use prefix is not decorative — React's linter uses it to enforce the rules of hooks. Past that, name the hook after the value it returns rather than its implementation.
3. Tuple for one or two values, object for three or more
A tuple lets the caller rename freely, which matters when a component uses the same hook twice. Past two elements, positional destructuring becomes a memory test.
as const or an explicit tuple type. Without it TypeScript widens the return to an array union and destructuring loses its types.4. Keep returned functions referentially stable
A function whose identity changes every render will invalidate every useMemo, useCallback and React.memo downstream, and re-run any effect that depends on it.
5. Clean up everything you start
Timers, listeners, observers, subscriptions and in-flight requests all need a cleanup function. React Strict Mode deliberately mounts, unmounts and remounts components in development specifically to expose the ones you forgot.
6. Put the callback in a ref, not the dependency array
If your hook takes a callback, do not depend on it directly — callers pass inline arrow functions, which change identity every render and would re-subscribe your listener each time.
7. Accept primitives, not object literals
An options object passed inline is a new reference every render. If your hook depends on it, the effect never stops re-running.
8. Make it work on the server
Even if you are not server rendering today, someone will move the app to Next.js eventually. Guard browser globals in the state initialiser and do the real read in an effect.
9. Type the generics, not the call sites
A well-typed hook means callers never write a type annotation. Infer from the arguments wherever you can.
10. Return loading and error, not just data
Any hook that does asynchronous work has at least three states. Returning only the happy path pushes the other two back onto every caller.
11. Test the hook, not a component wrapping it
renderHook from React Testing Library runs a hook in isolation, which keeps the test about behaviour rather than markup.
12. Do one thing
A hook that fetches, caches, paginates and manages a modal is four hooks. Small hooks compose; large ones get copied and diverge.
Key takeaway
If you cannot describe what a hook returns in a single sentence, it is doing too much. Split it until you can.
Can a custom hook call another custom hook?
Yes, and it is encouraged. Composition is how hooks stay small. The only constraint is the rules of hooks: every call must be at the top level of the function, never inside a condition or loop.
Should every custom hook live in its own file?
Yes, in a hooks directory, named after the hook. It makes them discoverable, keeps imports honest about what a module depends on, and lets bundlers tree-shake properly.
When should I use useReducer inside a custom hook?
When several pieces of state change together in response to the same events. A fetch hook tracking data, loading and error is a good example — a reducer makes the invalid combinations unrepresentable.
Do custom hooks share state between components?
No. Each component that calls a hook gets its own independent state. To share state you need context, an external store, or a browser-level store like localStorage — which is exactly how useLocalStorage manages to stay in sync across components.
Keep reading
The React Hooks Cheat Sheet
A complete reference to every built-in React hook and the 44 custom hooks in @danixsoft/hooks — what each one does, when to reach for it, and the mistake people make with it.
Stale closures, and how to stop them
Why your setInterval logs the same number forever, why your event handler sees old state, and the three patterns that fix it permanently.