React Custom Hooks — Write Reusable Logic for Scalable Applications
> cd .. / HUB_EDITORIALE
Sviluppo di siti web

React Custom Hooks — Write Reusable Logic for Scalable Applications

[2026-07-25] Author: Ing. Calogero Bono
> share
Zenithby Meteora Web The operating system for your business. Social, clients, bookings and invoices in one platform. Gyms, barbers, professionals. Discover Zenith Free demo · no card

Have you ever duplicated blocks of code across React components? A fetch, a form, a timer, a WebSocket connection, local state management repeated in three different places. We see it all the time in projects that come to us: business logic gets tangled inside components, code bloats, and bugs hide where you copy-pasted. Custom hooks solve exactly that: they extract repeated logic into reusable, testable functions independent of the UI.

We, at Meteora Web, have been working with React since 2017. Every time we start a new module — whether a dashboard, e-commerce, or social platform — the first question is: "what logic can we isolate in a custom hook?". Not just to write less code, but to have a single point of maintenance. If the authentication system changes, you update one hook and all components update. That's the power.

In this guide we'll walk through concrete patterns we use daily: from creating a data-fetching hook with loading/error states, to hooks for event listeners, forms, debounce, and advanced patterns like using refs and composing multiple hooks. No abstract theory: every example is copy-pasteable and already tested on our real-world projects.

What Problems Do Custom Hooks in React Solve and Why Are They Strategic?

Imagine three different components that need to fetch data from an API. Without custom hooks, you end up with three almost identical useEffect + useState blocks. Changing the fetch behavior — adding a timeout, handling errors differently, implementing caching — forces you to touch all components. With a custom hook, the logic lives in one place.

But it's not just about maintenance. Custom hooks enable you to:

  • Separate logic from UI: components stay declarative, hooks contain imperative logic.
  • Test in isolation: a hook can be tested with @testing-library/react-hooks without mounting a component tree.
  • Share logic across projects: extract a package of your custom hooks and reuse them across multiple apps.
  • Adhere to the DRY principle: never repeat the same logic twice.

We, at Meteora Web, have built an internal library of custom hooks for authentication, API calls, pagination, and error handling. Every new project starts from that base. The result? Fewer bugs, faster onboarding, more readable code.

Sponsored Protocol

How to Create a Custom Hook for Data Fetching with Loading and Error States

Let's start with the most common case: a hook that performs an async call and returns data, loading, and error. This pattern is the foundation of many modern React applications.

Minimal working code

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    let ignore = false;

    const fetchData = async () => {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch(url);

        if (!response.ok) {
          throw new Error(`Error ${response.status}`);
        }

        const result = await response.json();

        if (!ignore) {
          setData(result);
        }
      } catch (err) {
        if (!ignore) {
          setError(err.message);
        }
      } finally {
        if (!ignore) {
          setLoading(false);
        }
      }
    };

    fetchData();

    return () => {
      ignore = true;
    };
  }, [url]);

  return { data, loading, error };
}

What does this code do? It receives a URL, performs the fetch on mount (or when URL changes), and returns three values. The ignore variable prevents state updates on unmounted components (a common pitfall). Errors are caught and returned as a string.

Usage in a component

import React from 'react';
import { useFetch } from './hooks/useFetch';

function UserList() {
  const { data: users, loading, error } = useFetch('https://api.example.com/users');

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <ul>
      {users.map(user => (<li key={user.id}>{user.name}</li>))}
    </ul>
  );
}

Watch out for common mistakes: many forget to handle cleanup or reset loading on every new fetch. Our hook does it automatically. If you need manual refetch, you can expose a refetch function, but let's keep it simple for now.

Sponsored Protocol

How to Create a Custom Hook for Event Listeners (useEventListener)

Another pattern we use constantly is listening to global events (scroll, resize, outside click). React doesn't natively handle proper bind/unbind when a component unmounts. A custom hook solves it all.

import { useEffect, useRef } from 'react';

export function useEventListener(eventName, handler, element = window) {
  const savedHandler = useRef(handler);

  useEffect(() => {
    savedHandler.current = handler;
  }, [handler]);

  useEffect(() => {
    const isSupported = element && element.addEventListener;
    if (!isSupported) return;

    const eventListener = (event) => savedHandler.current(event);

    element.addEventListener(eventName, eventListener);

    return () => {
      element.removeEventListener(eventName, eventListener);
    };
  }, [eventName, element]);
}

Why use useRef to save the handler? To avoid re-running the useEffect every time the handler function changes. savedHandler.current is always up-to-date, but the event is bound only once. This is an advanced pattern that reduces unnecessary re-binds.

Practical example: detect clicks outside a dropdown.

import { useRef } from 'react';
import { useEventListener } from './hooks/useEventListener';

function Dropdown({ onClose }) {
  const ref = useRef(null);

  useEventListener('mousedown', (event) => {
    if (ref.current && !ref.current.contains(event.target)) {
      onClose();
    }
  });

  return <div ref={ref}>...dropdown content...</div>;
}

How to Manage Form State with a Custom useForm Hook

Forms are the bread and butter of web projects. A custom hook for managing field state saves us hours.

Sponsored Protocol

import { useState } from 'react';

export function useForm(initialValues) {
  const [values, setValues] = useState(initialValues);

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    const newValue = type === 'checkbox' ? checked : value;
    setValues(prev => ({ ...prev, [name]: newValue }));
  };

  const reset = () => setValues(initialValues);

  return [values, handleChange, reset];
}

Usage:

import { useForm } from './hooks/useForm';

function LoginForm() {
  const [formValues, handleChange, reset] = useForm({ email: '', password: '' });

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(formValues);
    reset();
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" value={formValues.email} onChange={handleChange} />
      <input name="password" type="password" value={formValues.password} onChange={handleChange} />
      <button type="submit">Login</button>
    </form>
  );
}

This basic hook can be extended with validation, nested field handling, debounced onChange — we use it as a starting point for every form.

Custom Hook for Debounce — Avoid API Calls on Every Keystroke

If you have a search bar that filters results as the user types, making an API call on every keystroke is a disaster. Debounce delays the call until the user stops typing for a given interval.

import { useState, useEffect } from 'react';

export function useDebounce(value, delay = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);

    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

How to use it with useFetch:

Sponsored Protocol

import { useState } from 'react';
import { useDebounce } from './hooks/useDebounce';
import { useFetch } from './hooks/useFetch';

function SearchUsers() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 400);
  const { data: users, loading, error } = useFetch(`/api/users?q=${debouncedQuery}`);

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search user" />
      {/* ... display results */}
    </div>
  );
}

Debounce integrates perfectly with the fetch hook: it avoids unnecessary calls and improves user experience.

Advanced Patterns: Composing Hooks and Using useRef for Mutable Values

Once you're comfortable, you can combine multiple custom hooks into one. For example, a useFetchWithDebounce hook that merges useDebounce and useFetch. Or useOnlineStatus that uses useEventListener internally.

import { useState, useCallback } from 'react';
import { useEventListener } from './useEventListener';

export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEventListener('online', () => setIsOnline(true));
  useEventListener('offline', () => setIsOnline(false));

  return isOnline;
}

Another advanced pattern is using useRef to keep track of values that should not cause re-renders. For example, a timer that must be cleaned up on unmount.

import { useRef, useEffect, useCallback } from 'react';

export function useInterval(callback, delay) {
  const savedCallback = useRef(callback);

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    if (delay === null) return;

    const id = setInterval(() => savedCallback.current(), delay);

    return () => clearInterval(id);
  }, [delay]);
}

This hook prevents the setInterval from being reset on every render of the callback, keeping performance top-notch.

Sponsored Protocol

Common Mistakes to Avoid with Custom Hooks

  • Don't call hooks inside conditions or loops: custom hooks, like all React hooks, must be called unconditionally and in stable order.
  • Never skip cleanup in effects: if your hook uses useEffect with subscriptions (event listeners, interval, fetch), always provide a cleanup function.
  • Avoid returning oversized objects: if a hook returns many values, consider splitting it into smaller hooks (separation of concerns).
  • Document the contract: every custom hook is a function with inputs and outputs. Document what it expects and returns. You do it for your team too.

What to Do Now — Immediate Actions

  1. Identify repeated logic: open your React codebase and look for useState + useEffect blocks that appear in at least two components. Extract them into a custom hook.
  2. Create a hooks/useFetch.js file with the code above and replace all manual fetches in your components.
  3. Add a useDebounce hook to your search bar. Measure the difference in API calls using developer tools.
  4. Refactor gradually: don't rewrite everything in one night. Start with a simple pattern (fetch, events) and then expand.
  5. Test: write a test for your custom hook using @testing-library/react-hooks. It guarantees stable behavior.

We, at Meteora Web, have seen projects transform from a tangle of components into clean, maintainable code thanks to custom hooks. It's not just a best practice — it's an investment in your software quality.

For a complete overview of React, check out our React.js Pillar.

> share
Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere informatico, fondatore di Meteora Web e Zenith OS. System administrator e progettista di piattaforme, app e CMS proprietari, con esperienza in sviluppo full-stack, marketing digitale ed ecosistema Google.
[ Read Full Dossier ]

> METEORA_WEB // DIGITAL AGENCY

We build the digital presence your business deserves.

Websites, social media, online advertising, e-commerce and high-performance hosting, engineered with method by computer engineers in Sciacca, for all of Italy.

> MW_JOURNAL

> READ_ALL()