How to Create Custom Hook in React

At one point, I found myself copying the same state management and effect logic across multiple components. It started off harmless—a little useState here, a useEffect there. But soon, my components began to feel cluttered and repetitive. I realized I was solving the same problem in the same way, but in different places. That’s when I knew I needed to extract that logic into a custom hook. Not only did it clean up my components, but it also helped me write reusable and testable code that made my workflow significantly smoother.

Start with a Pattern

Most of the custom hooks I’ve written started with a repeated pattern. For example, I was frequently fetching data from APIs, handling loading and error states, and managing the result. Rather than rewriting that logic every time, I decided to isolate it. I created a file called useFetch.js and began by pulling out the logic from one of my components.

It looked something like this:

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    let isMounted = true;
    setLoading(true);
    fetch(url)
      .then((res) => res.json())
      .then((data) => {
        if (isMounted) {
          setData(data);
          setLoading(false);
        }
      })
      .catch((err) => {
        if (isMounted) {
          setError(err);
          setLoading(false);
        }
      });

    return () => {
      isMounted = false;
    };
  }, [url]);

  return { data, loading, error };
}

The component that used to hold all this now just called useFetch('/api/posts'), and all the state and side-effect logic was offloaded to the hook. This simple change instantly made the component more readable.

Keep It Pure

While writing custom hooks, I make it a point to keep them pure. That means no direct DOM manipulations or side effects that aren’t explicitly controlled. Hooks are meant to be composable and reusable, and introducing external side effects inside them makes that harder.

There was a time when I tried to manipulate focus or scroll inside a hook without thinking about cleanup, and it led to inconsistent behavior. I’ve since learned that if a hook does need to interact with the browser environment, I isolate it carefully and make sure all side effects are controlled with proper dependencies and cleanup.

Accept Parameters

A powerful feature of custom hooks is their ability to accept parameters. I don’t hardcode values inside the hook unless they are truly static. Instead, I expose those values to be passed in by the calling component.

For instance, I once needed a countdown timer that could start from any number of seconds. Instead of locking the logic inside a component, I built a hook like useCountdown(initialSeconds) and exposed functions like start, pause, and reset. Having hooks behave like configurable tools made them much more flexible.

Return What Matters

One thing I learned the hard way is not to return everything from a hook unless it’s needed. Overexposing internals can be tempting, but it leads to tightly coupled components. When I write hooks, I carefully choose what the hook should return—only what the component needs to use.

For example, in a useForm hook, instead of returning all internal useState variables, I return values like formData, handleChange, and resetForm. This made the hook feel like a black box that just worked—without cluttering the consuming component with implementation details.

Compose Other Hooks

One of my favorite discoveries was that custom hooks can use other hooks. This unlocked a ton of possibilities. I created a hook that handled keyboard shortcuts using useEffect, another one that handled user preferences using useState and useEffect, and then built a higher-level useDashboard hook that combined both.

This idea of composition gave me a clean architecture to work with. I wasn’t reinventing logic—I was layering it. It reminded me of building small utilities and then combining them, just like I once did when I restructured table design in a React component to improve layout flexibility.

Handle Cleanup

Any time a hook involves a side effect, I make sure to return a cleanup function. I learned this lesson while handling event listeners inside a hook. I had a useWindowResize hook that added a listener to window.resize but didn’t remove it on unmount. This caused memory leaks and unexpected behavior when components were remounted.

Now, it’s second nature to return a cleanup function inside useEffect, especially when dealing with timers, subscriptions, or global event listeners.

useEffect(() => {
  const handleResize = () => setSize(window.innerWidth);
  window.addEventListener('resize', handleResize);

  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

That little return statement saves a lot of headaches.

Follow Naming Convention

I stick to the useSomething naming convention for all custom hooks. Not just because React expects it—but because it instantly signals what kind of logic I’m importing. When I’m scanning a component and see useFormValidation, I know it’s a hook and not a component or a utility function.

It also helped me in situations where tools like the React DevTools or ESLint could better understand my code. Even when I was debugging deeply nested logic, naming consistency helped me trace behaviors faster.

Test in Isolation

Hooks are logic-focused. So when I’m writing something complex—like a debounced search hook—I always test the hook in isolation before integrating it into components. There was a time I struggled to figure out why a component using useDebounce wasn’t updating as expected. Once I pulled the hook out and tested it independently, I found the debounce delay was never getting cleared.

Testing hooks separately has made my integration smoother. Even when using advanced logic like query clients in client components, isolating the hook logic gave me a clear understanding of what was working and what wasn’t.

Keep it Small

I used to get excited and over-engineer hooks. What started as a useScrollDirection hook ended up tracking scroll position, direction, velocity, and more—all in one file. I’ve since learned to keep each hook focused. If it does too much, I split it into multiple hooks. A focused hook is easier to maintain, test, and understand.

If I ever need combined behavior, I just create a new hook that uses two or three of the smaller ones inside it.

Final Thought

Building custom hooks in React felt like unlocking a new level of abstraction. It helped me take control of my app’s logic, reuse patterns that made sense, and build components that were lean and readable. Instead of stuffing state and effects into every component, I now treat logic as something I can extract, compose, and reuse like any other part of the system.

Each hook I write becomes a quiet improvement to the developer experience—mine and anyone else’s who touches the code. And once you write a few, it becomes hard to imagine working without them.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top