React

Building Custom Hooks in React

Thirdy Gayares
14 min read

πŸŽ“ What You Will Learn

  • What a custom hook is: a function that starts with use and calls other hooks
  • useToggle: your first custom hook, reused across independent components
  • useLocalStorage: syncing state with a browser API via useEffect
  • useDebounce: delaying a value update until input settles
  • useFetch: composing useState + useEffect into a data hook
  • Rules for composing hooks: what's safe, and the mistakes that break them
1

What a Custom Hook Actually Is

A custom hook is just a JavaScript function. Nothing magic β€” no special API to register it. Two conventions make it a "hook" React recognizes: its name starts with use, and it calls other hooks (useState, useEffect, etc.) inside itself.

The entire point is reusing stateful logic β€” not UI. If you've ever copy-pasted the same useState + useEffect combo into three different components, that logic wanted to be a custom hook.

anatomy.jsx
// This IS a custom hook β€” starts with "use", calls other hooks
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return width;
}

// Any component can now reuse this logic in one line
function Header() {
  const width = useWindowWidth();
  return <p>Window is {width}px wide</p>;
}
2

Your First Custom Hook: useToggle

The simplest possible custom hook wraps a single useState call with a bit of convenience logic. It still pays off immediately: every component that needs a boolean toggle gets the exact same, tested behavior.

Sidebar is open
Preview theme: light

Same useToggle() hook, two completely independent pieces of state.

Notice the two toggles never interfere with each other β€” each call to useToggle() creates its own, completely independent useState underneath.
3

Syncing with a Browser API: useLocalStorage

A custom hook is the natural place to bridge React state with a non-React API β€” here, localStorage. Read once on mount (lazily, via useState's initializer function), then write back on every change.

Saved to localStorage as you type. Refresh this page β€” it'll still be here.

Guard against SSR. window doesn't exist during server rendering β€” check typeof window === "undefined" before touching localStorage, or the initializer will crash on the server.
4

Timing Logic: useDebounce

Debouncing β€” waiting for input to "settle" before acting on it β€” is exactly the kind of fiddly useEffect + timer logic you don't want to rewrite in every search box.

Live value: (empty)

Debounced (500ms after you stop typing): (empty) β€” this is what you'd send to a search API

5

Composing Hooks: useFetch

Custom hooks can call other hooks β€” including other custom hooks. useFetch below composes useState and useEffect into a single reusable data-fetching hook with loading/error/success states baked in.

⏳ Loading user #1…
The cancelled flag matters: if userId changes before the previous fetch resolves, the cleanup function flips cancelled to true so the stale response never overwrites the newer one.
6

Rules for Composing Hooks Safely

Custom hooks still have to follow the same Rules of Hooks as built-in ones, because under the hood they're calling useState/useEffect just like any component would:

  • Only call hooks (custom or built-in) at the top level β€” never inside conditions, loops, or nested functions
  • Only call hooks from React function components or other custom hooks β€” never from a plain utility function
  • Name every custom hook starting with use so linters and other developers can enforce these rules on it too
7

Common Mistakes

1Not prefixing with 'use'
mistake-naming.jsx
// ❌ BAD: the linter can't tell this calls hooks, rules won't be enforced
function toggle(initial) {
  const [value, setValue] = useState(initial); // breaks the rules silently
  return [value, () => setValue(v => !v)];
}

// βœ… GOOD: the "use" prefix is what makes it a recognized hook
function useToggle(initial) {
  const [value, setValue] = useState(initial);
  return [value, () => setValue(v => !v)];
}
2Forgetting cleanup in a hook that subscribes to something
mistake-no-cleanup.jsx
// ❌ BAD: every remount adds another listener that's never removed
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    window.addEventListener("resize", () => setWidth(window.innerWidth));
  }, []);
  return width;
}

// βœ… GOOD: return the cleanup function from useEffect
useEffect(() => {
  const handler = () => setWidth(window.innerWidth);
  window.addEventListener("resize", handler);
  return () => window.removeEventListener("resize", handler);
}, []);
3Making a hook do too much
mistake-god-hook.jsx
// ❌ BAD: one hook handling auth AND theme AND cart β€” hard to reuse or test
function useEverything() { /* ...200 lines... */ }

// βœ… GOOD: small, focused hooks that compose
function useAuth() { /* ... */ }
function useTheme() { /* ... */ }
function useCart() { /* ... */ }
8

Best Practices

βœ…Extract logic you've copy-pasted twice β€” that's the real signal to make a hook
βœ…Return the same shape as useState ([value, setter]) when it fits β€” familiar API
βœ…Keep each hook focused on one concern β€” compose several small hooks over one giant one
βœ…Clean up subscriptions/timers inside any hook that creates them
βœ…Test hooks like functions β€” they don't need a full component to verify their logic
9

Common Custom Hook Reference

HookWrapsPurpose
useToggleuseStateBoolean flip-flop state
useLocalStorageuseState + useEffectState synced to a browser storage API
useDebounceuseState + useEffectDelay reacting to a fast-changing value
useFetchuseState + useEffectData fetching with loading/error/success
usePrevioususeRef + useEffectRead last render's value (see the useRef post)
10

Practice Project

Build a useOnlineStatus() custom hook that:

  • Returns a boolean tracking navigator.onLine
  • Subscribes to the browser's online and offline window events
  • Cleans up both listeners when the component using it unmounts
  • Is reused in two places at once β€” a header badge and a "you're offline" banner β€” to prove they share nothing but the pattern
11

What's Next?

πŸš€ Next Learning Topics:
  • useMemo / React.memo: optimize hooks and components that do expensive work
  • Data Fetching: graduate from a hand-rolled useFetch to TanStack Query
  • State Management: when a custom hook isn't enough and you need a real global store
  • React Hooks Deep Dive: revisit how every built-in hook fits together

Keep practicing! πŸ’ͺ Custom hooks are where React composition really shines β€” once you start noticing repeated useState/useEffect pairs, extracting them makes every component that uses them shorter and easier to trust.

About the Author

TG

Thirdy Gayares

Passionate developer creating custom solutions for everyone. I specialize in building user-friendly tools that solve real-world problems while maintaining the highest standards of security and privacy.