React

React Custom Hooks👨‍💻

Custom hooks are functions that start with use and call other hooks. That's it. There's no special API, no registration, no magic — just a function that encapsulates stateful logic so you don't copy-paste the same useState + useEffect combo across ten components.

The heuristic is straightforward: when you see the same combination of hooks duplicated in two or more components, extract it. Custom hooks are React's composition model, and they're the reason hook-based code scales better than class mixins or render props ever did.

Key Takeaways

  • 1A custom hook is a plain JavaScript function whose name starts with `use` — this naming convention enables the React linter to enforce the Rules of Hooks
  • 2Each component that calls a custom hook gets its own isolated copy of the state — hooks share logic, not state
  • 3Return a tuple `[value, setter]` when your hook manages a single piece of state (like useState), return an object when there are 3+ values
  • 4Wrap action functions in `useCallback` so consumers can safely include them in dependency arrays without causing infinite re-renders
  • 5Always handle SSR: check `typeof window !== 'undefined'` before accessing browser APIs like localStorage, matchMedia, or IntersectionObserver
  • 6Custom hooks compose — `useUserPosts` can call `useUser` which calls `useFetch`, building complex behavior from simple primitives

Master react custom hooks

Take the Building Custom Hooks course with hands-on lessons and challenges.

Examples

useLocalStorage — persist state across sessions

tsx

This follows the useState tuple pattern so the API feels familiar. The lazy initializer reads from localStorage only on mount. The storage event listener syncs the value when another tab writes to the same key — a detail most implementations miss.

useDebounce — delay value updates for search inputs

tsx

The hook debounces any value, not just strings. Each time value changes, the previous timer is cleared and a new one starts. The consumer doesn't need to know about timeouts — it just uses the debounced value as a dependency. This is the standard pattern for search-as-you-type.

useMediaQuery — respond to CSS breakpoints in JS

tsx

Uses the matchMedia API which fires a change event only when the query result flips, so there's no polling or resize listener overhead. The SSR guard returns false by default — on the server, we assume the non-matching state and let the client hydrate correctly.

useAsync — manage loading, error, and data for any promise

tsx

The discriminated union for AsyncState means TypeScript narrows the types correctly — when status is 'success', data is guaranteed to be T, not T | null. The deps array re-triggers the fetch when inputs change, similar to useEffect's dependency list. For production apps you'd add AbortController support to cancel in-flight requests on unmount.

useIntersectionObserver — lazy loading and scroll animations

tsx

The freezeOnceVisible option is the key detail: once the element enters the viewport, the observer disconnects and the state never flips back. This is what you want for lazy loading and entrance animations. The ref is set via a callback (useState) rather than useRef so the observer re-attaches when the DOM element changes.

useClickOutside — close dropdowns and modals

tsx

Returns the ref directly so the consumer just attaches it to the container element. Listens on mousedown instead of click so the dropdown closes before the click event propagates — this prevents ghost clicks on elements that appear under the dropdown. The touchstart listener handles mobile.

Common Mistakes

Mistake:

Returning a new object or array on every render without memoization — e.g. `return { value, toggle }` where toggle is recreated each time

Fix:

Wrap action functions in `useCallback` and memoize computed objects with `useMemo` if needed. Consumers will put your return values in dependency arrays, and unstable references cause infinite re-render loops.

Mistake:

Calling hooks conditionally inside a custom hook — e.g. `if (enabled) { useState(...) }` — which violates the Rules of Hooks

Fix:

Always call every hook unconditionally. Use the condition inside the hook's logic instead: call useState regardless, and gate the effect body with `if (!enabled) return;`.

Mistake:

Accessing browser APIs (localStorage, window.matchMedia, IntersectionObserver) without an SSR guard, causing 'window is not defined' crashes in Next.js

Fix:

Check `typeof window !== 'undefined'` in your initializer, or move browser API access into a useEffect (which only runs on the client). Provide a sensible default for the server render.

Mistake:

Not cleaning up subscriptions, event listeners, or timers in the effect's return function — causing memory leaks and state updates on unmounted components

Fix:

Every `addEventListener` needs a matching `removeEventListener`. Every `setTimeout` needs `clearTimeout`. Every `observer.observe()` needs `observer.disconnect()`. Return a cleanup function from useEffect, always.

Best Practices

  • Extract a custom hook when the same useState + useEffect pattern appears in two or more components — don't preemptively abstract a one-off
  • Follow the naming convention: `useXxx` for hooks that manage state, `useOnXxx` for event-based hooks — the `use` prefix is what makes the linter work
  • Keep hooks focused on one concern: `useLocalStorage` manages persistence, `useDebounce` manages timing — don't build a god hook that does both
  • Accept a config object for optional parameters instead of positional arguments: `useFetch(url, { enabled, refetchInterval })` is clearer than `useFetch(url, true, 5000)`
  • Write at least one test per hook using `renderHook` from @testing-library/react — hooks are pure logic with no UI, which makes them the easiest part of your app to test

Summary

Custom hooks are React's answer to code reuse. They're plain functions that call other hooks, and each component gets its own isolated state. The practical hooks you'll reach for most often are useLocalStorage (persisted state), useDebounce (rate-limiting), useMediaQuery (responsive logic), useAsync (data fetching states), useIntersectionObserver (viewport detection), and useClickOutside (dismissing overlays). Extract them when you see duplication, keep them focused on one job, and wrap returned functions in useCallback so consumers don't fight dependency arrays.

Practice React with hands-on challenges

Learn react custom hooks hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master React with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.