NB logo
Image of retro 1950s-style illustration of a woman working on a laptop, with the React atom logo beside her and the title “Building Custom Hooks” in large bold text inside a red block, with “Building” in smaller text above

Building Custom Hooks the Right Way

profile

Nitesh Babu

30 August 2025

If you spend enough time with React, something interesting happens inside your components. The UI stays small but the behaviour keeps growing. You add a fetch call, a debouncer, a local event listener and maybe a visibility tracker. None of these are wrong by themselves, but put together, the component slowly turns into a storage room where logic goes to hide.

This is the exact moment custom hooks start making sense.

Custom hooks are React’s answer to logic reuse. They don’t render anything, they don’t care about UI, and they have no opinions about markup. Their job is simple. Extract behaviour. Keep your component clean. Make the logic portable. If you want the official definition, check React’s official doc here.

The basic form of a custom hook is deceptively small.

import { useState, useEffect } from "react"

export function useIsMounted() {
  const [mounted, setMounted] = useState(false)

  useEffect(() => {
    setMounted(true)
  }, [])

  return mounted
}

It looks almost too tiny to matter, but in real codebases these small nuggets appear everywhere. Avoiding hydration mismatches, toggling UI only on the client, improving SSR behaviour. A hook like this is the difference between an expressive component and one drowning in conditionals.

To build hooks properly, you need one mental model. A good hook hides complexity on the inside and exposes clarity on the outside. The caller shouldn’t care about timers, cleanup functions or internal refs. It should simply receive something useful.

Fetching is the classic example everyone gets wrong at first.

import { useState, useEffect } from "react"

export function useFetch(url, options = {}) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    let active = true

    async function load() {
      try {
        setLoading(true)
        const res = await fetch(url, options)
        if (!active) return
        setData(await res.json())
      } catch (err) {
        if (!active) return
        setError(err)
      } finally {
        if (active) setLoading(false)
      }
    }

    load()
    return () => { active = false }
  }, [url])

  return { data, loading, error }
}

This looks like a lot, but that is exactly the point. The hook carries the complexity. The component gets simplicity.

const { data, loading, error } = useFetch("/api/posts")

if (loading) return <p>Loading…</p>
if (error) return <p>Failed: {error.message}</p>

return <PostList posts={data} />

Another pattern you run into frequently is browser events. Scroll listeners, mouse tracking, keypress listeners. Putting them directly inside a component works until the third or fourth one. After that you begin to feel the weight.

import { useEffect } from "react"

export function useEvent(event, handler, element = window) {
  useEffect(() => {
    element.addEventListener(event, handler)
    return () => element.removeEventListener(event, handler)
  }, [event, handler, element])
}

And then the component reads like a sentence.

useEvent("scroll", () => console.log("scrolling…"))

There’s something pleasing about the readability that custom hooks create when done right.

Another useful example is a simple toggle hook. It’s the kind of behaviour you write repeatedly without noticing.

import { useState, useCallback } from "react"

export function useToggle(initial = false) {
  const [value, setValue] = useState(initial)

  const toggle = useCallback(() => {
    setValue(v => !v)
  }, [])

  return [value, toggle]
}

When you wrap these pieces inside a hook, your component regains its clarity.

const [open, toggleOpen] = useToggle()

<button onClick={toggleOpen}>Toggle</button>
{open && <Sidebar />}

The trick to mastering custom hooks isn’t memorising patterns. It’s recognising when logic stops belonging to the UI. Whenever your component starts juggling conditions, timers or repeated behaviour, the logic has reached the point where it deserves a new home.

Here’s an image that captures the idea visually, even if it doesn’t load.

Custom hooks are not about making your code fancy. They’re about protecting the mental model of your components. UI on the outside. Behaviour on the inside. If you design hooks with that philosophy, the architecture of your React app becomes more predictable, more maintainable and much easier to think about.

🔗 Share this post

Spread the word wherever you hang out online.

Related Posts

Be part of the thinking behind the craft.

Why real systems behave the way they do. Technical paths that change outcomes.
The web’s native language, decoded.

Monthly. No pandering, no gimmicks, no shallow summaries.