Stockio

How to Create Custom Hooks in React Without Coupling

Design custom React hooks with clear contracts, reliable effect cleanup, and focused tests, while avoiding shared-state confusion, stale closures, and unnecessary abstractions.

By Sofia Marchetti

JavaScript Engineer ·

On this page

A custom hook should make a component easier to understand without hiding important behavior. Moving thirty lines into a function named useManager does not automatically create a useful abstraction. The best hooks describe a stable capability, expose a small contract, and make subscriptions, ownership, and failure behavior predictable to their callers.

Start with a concrete contract

Before extracting code, write one sentence describing the capability: observe a media query, synchronize a subscription, or manage a dismissible disclosure. If the sentence lists unrelated responsibilities, keep those responsibilities separate. A hook combining payment submission, page navigation, analytics, and toast presentation is difficult to reuse because every consumer inherits all four policies.

For hooks that consume API records, use JSON to TypeScript with sanitized sample payloads to sketch the input and return types. Review nullable fields and optional properties against the actual service contract. Generated interfaces document assumptions; they do not validate responses at runtime or decide which component should own a request.

Define inputs and outputs before implementation. A disclosure hook might accept an initial state and return the current state plus explicit open and close actions. Avoid returning every internal setter merely because it exists. Small interfaces let implementations change without forcing callers to understand the mechanism. The development hub offers related implementation resources when the abstraction crosses into other concerns.

Share logic, not accidental global state

Each invocation of a custom hook gets its own React state. Calling useDisclosure in two components does not synchronize their expanded values. To share a value, lift state to a common owner, provide it through context, or subscribe to an external store designed for shared ownership.

This distinction is central to state ownership. A hook may wrap access to a shared store, but the store is shared because of where it lives, not because the wrapper's name starts with use. Module-level mutable variables are usually a poor shortcut, especially in server-rendered applications where multiple requests may share a process.

Follow the Rules of Hooks: call hooks at the top level of React components or other hooks, not inside conditional branches or event callbacks. A conditional feature can still use a hook with an enabled input, provided the hook itself remains called consistently and handles the disabled behavior internally.

Do not extract code solely because two components contain similar lines. Similar syntax can conceal different requirements. Wait until the shared behavior has a meaningful contract, then extract that contract rather than forcing both consumers into an increasingly configurable universal hook.

Model external subscriptions explicitly

Browser capabilities are good candidates for focused hooks because they have identifiable subscription and cleanup behavior. The following hook observes one media query using React's external-store API. Its server snapshot deliberately returns false, so consumers must not depend on it for essential server-rendered content.

import { useCallback, useSyncExternalStore } from "react";

const getServerSnapshot = () => false;

export function useMediaQuery(query: string) {
const subscribe = useCallback((notify: () => void) => {
  const media = window.matchMedia(query);
  media.addEventListener("change", notify);
  return () => media.removeEventListener("change", notify);
}, [query]);

const getSnapshot = useCallback(
  () => window.matchMedia(query).matches,
  [query],
);

return useSyncExternalStore(
  subscribe,
  getSnapshot,
  getServerSnapshot,
);
}

The subscription contract is precise: attach a listener for the current query and remove that exact listener when unsubscribing. The snapshot is a boolean, which satisfies the requirement for a stable value when nothing changed. Returning a fresh object on every snapshot read would require caching to avoid repeated updates.

The useSyncExternalStore reference explains server snapshots and hydration expectations. For an actual shared store, the server snapshot must reflect the initial data supplied to the client. The constant false here is a deliberate fallback for a browser-only observation, not a general recipe for hydrating application state.

Prefer CSS media queries when the requirement is purely visual layout. JavaScript observation is more appropriate when behavior genuinely changes. This avoids a post-hydration layout decision and keeps simple styling independent of React execution.

Make effects and asynchronous work honest

An effect should synchronize with something external, not serve as a generic place to calculate values that could be derived during render. Read React's guidance on custom hooks before creating wrappers such as useMount that hide dependencies and make synchronization appear to happen only once.

Dependency arrays describe reactive values used by an effect. Suppressing the lint rule to preserve an old callback often creates stale behavior rather than stability. If an object argument causes repeated work, first ask whether the effect should depend on its primitive fields or whether that object represents a genuinely changing external resource.

For request hooks, specify cancellation and ordering. Aborting a fetch during cleanup prevents unnecessary work, but some asynchronous operations cannot be cancelled. In those cases, ignore results belonging to superseded requests so an older response cannot overwrite newer state. Distinguish an intentional abort from a failure users need to see.

Be explicit about callback identity only when consumers need it. Memoizing every returned function adds complexity without guaranteeing useful performance. Conversely, a callback documented for use in another effect should not change on every render without reason. Treat identity guarantees as part of the public contract and test them when consumers rely on them.

Test behavior through realistic consumers

Test the hook through a small component or an appropriate hook-testing helper. Verify observable behavior rather than inspecting private state slots. For a media-query hook, simulate the initial match, dispatch a change, update the query, and confirm that unmount removes listeners. Include a server-rendering scenario if your application uses server rendering.

Run tests with Strict Mode where practical. Development setup and cleanup checks can reveal subscriptions that are not properly reversible. A test that passes only because an effect runs once is not evidence that the synchronization logic is robust.

Also test two independent consumers. Unless sharing is intentional, updating one should not modify the other. For request hooks, resolve promises out of order and verify that the newest request wins. Check failures and retries, not merely the successful first response.

Document limitations next to usage examples: browser support, server fallback, error policy, and whether the caller owns persistence. Review keyboard and state announcements using the accessible interface guide when a hook drives interactive UI. Reusable logic is valuable only when the resulting interface remains understandable to people using it.

Key takeaways

  • Extract capabilities, not miscellaneous component code with a convenient name.
  • Each hook invocation owns independent state unless it explicitly subscribes to shared ownership.
  • Keep subscriptions reversible and snapshots stable; document server behavior.
  • Treat dependencies, cancellation, and result ordering as correctness requirements.
  • Test multiple consumers, cleanup, failures, and the contract callers actually depend on.
Featured sponsor

Reach designers & developers at work

Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.

Become a sponsor

Keep reading

Related articles

More in JavaScript & Frameworks
JavaScript & Frameworks

10 Best TypeScript Learning Resources

Choose a practical path into TypeScript with official guides, books, interactive courses, and exercises for JavaScript learners, plus tips for practising types against realistic data.

Includes: JSON to TSSofia MarchettiSofia Marchetti
JavaScript & Frameworks

10 Best Free React Courses (Learn React for Free)

Choose a free React learning path that matches your experience, from official lessons and guided exercises to full projects. See what each resource teaches and what you should know first.

Includes: JSON to TSSofia MarchettiSofia Marchetti
JavaScript & Frameworks

10 Best React Frameworks

Compare ten React frameworks for websites, web apps, and full-stack projects. Learn which fits your rendering needs, hosting plans, team experience, and appetite for configuration.

Includes: JSON to TSDaniel KimDaniel Kim
JavaScript & Frameworks

Vue vs React: How to Choose for a New Project

Compare Vue 3 and React through state models, team workflows, full-stack options, and a practical prototype exercise that turns framework preferences into a defensible project decision.

Includes: JSON to TSSofia MarchettiSofia Marchetti
JavaScript & Frameworks

TypeScript Type Guards and Type Narrowing

Use control-flow analysis, discriminated unions, and honest custom type guards to turn uncertain values into dependable types without unsafe assertions.

Includes: JSON to TSSofia MarchettiSofia Marchetti
JavaScript & Frameworks

Typed Component Manifest for Your Design System

Build a typed component manifest for library catalogs, ownership checks, and documentation links, while separating generated TypeScript types from runtime validation.

Includes: JSON to TSDaniel KimDaniel Kim