Hai mai duplicato blocchi di codice tra componenti React? Un fetch, un form, un timer, una connessione WebSocket, la gestione dello stato locale che si ripete in tre punti diversi. Lo vediamo spesso nei progetti che arrivano da noi: la logica di business resta impigliata nei componenti, il codice si allunga, i bug si nascondono dove hai copiato e incollato. Custom hooks risolvono esattamente questo: estraggono la logica ripetuta in funzioni riutilizzabili, testabili e indipendenti dalla UI.
Noi, di Meteora Web, lavoriamo con React dal 2017. Ogni volta che iniziamo un nuovo modulo — che sia una dashboard, un e-commerce o una piattaforma social — la prima domanda è: "quale logica possiamo isolare in un custom hook?". Non solo per scrivere meno codice, ma per avere un unico punto di manutenzione. Se il sistema di autenticazione cambia, modifichi un hook e tutti i componenti si aggiornano. Questa è la potenza.
In questa guida vedremo i pattern concreti che usiamo ogni giorno: dalla creazione di un hook per il fetching dati con stato di caricamento/errore, a hook per event listener, form, debounce, fino a pattern avanzati come l'uso di ref e la composizione di più hook. Niente teoria astratta: ogni esempio è copia-incollabile e già testato sui nostri progetti reali.
Quali Problemi Risolvono i Custom Hooks in React e Perché Sono Strategici?
Immagina di avere tre componenti diversi che devono caricare dati da un'API. Senza custom hooks, ti ritrovi con tre blocchi di useEffect + useState quasi identici. Una modifica al comportamento di caricamento — introdurre un timeout, gestire errori diversi, aggiungere caching — ti costringe a toccare tutti i componenti. Con un custom hook, la logica vive in un solo posto.
Ma non è solo questione di manutenzione. I custom hooks permettono di:
- Separare la logica dalla UI: i componenti restano dichiarativi, gli hook contengono la parte imperativa.
- Testare in isolamento: un hook si testa con
@testing-library/react-hookssenza dover montare un albero di componenti. - Condividere logica tra progetti: puoi estrarre un pacchetto di hook personali e riutilizzarlo su più App.
- Aderire al principio DRY: non ripetere mai la stessa logica due volte.
Noi, di Meteora Web, abbiamo costruito una libreria interna di custom hooks per autenticazione, chiamate API, paginazione e gestione errori. Ogni nuovo progetto parte da quella base. Il risultato? Meno bug, onboarding più veloce, codice più leggibile.
Sponsored Protocol
Come Creare un Custom Hook per Fetch Dati con Gestione Caricamento ed Errore
Partiamo dal caso più comune: un hook che esegue una chiamata asincrona e restituisce dati, loading ed errore. Questo pattern è la base di molte applicazioni React moderne.
Il codice minimo funzionante
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 };
}
Cosa fa questo codice? Riceve un URL, esegue la fetch al mount (o quando l'URL cambia) e restituisce tre valori. La variabile ignore evita aggiornamenti di stato su componenti smontati (un errore comune). L'errore viene catturato e restituito come stringa.
Utilizzo in un componente
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>Caricamento...</p>;
if (error) return <p>Errore: {error}</p>;
return (
<ul>
{users.map(user => (<li key={user.id}>{user.name}</li>))}
</ul>
);
}
Attenzione agli errori comuni: molti dimenticano di gestire il clean-up o di resettare loading a ogni nuova chiamata. Il nostro hook lo fa automaticamente. Se vuoi supportare l'invocazione manuale (refetch), puoi esporre una funzione refetch, ma per ora teniamolo semplice.
Sponsored Protocol
Come Creare un Custom Hook per Event Listener (useEventListener)
Un altro pattern che usiamo continuamente è l'ascolto di eventi globali (scroll, resize, click fuori dal componente). React non gestisce nativamente il bind/unbind corretto quando un componente si smonta. Un custom hook risolve tutto.
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]);
}
Perché usare useRef per salvare la funzione? Per evitare di dover rieseguire il useEffect ogni volta che la funzione handler cambia. savedHandler.current è sempre aggiornato, ma l'evento rimane legato una volta sola. Questo è un pattern avanzato che riduce i re-bind inutili.
Esempio pratico: rilevare click fuori da un 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>;
}
Come Gestire Form e Stato Input con un Custom Hook useForm
I form sono il pane dei progetti web. Un custom hook per la gestione dello stato dei campi ci fa risparmiare ore.
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];
}
Utilizzo:
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>
);
}
Questo hook base può essere esteso con validazione, handling di campi annidati, debounce su onChange — lo usiamo come punto di partenza per ogni form.
Custom Hook per Debounce — Evitare Chiamate API a Ogni Tasto
Se hai una search bar che filtra risultati mentre l'utente digita, fare una chiamata API a ogni keystroke è un disastro. Il debounce ritarda la chiamata fino a quando l'utente smette di digitare per un determinato intervallo.
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;
}
Come usarlo con 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="Cerca utente" />
{/* ... mostra risultati */}
</div>
);
}
Il debounce si integra perfettamente con l'hook di fetch: evita chiamate inutili e migliora l'esperienza utente.
Pattern Avanzati: Comporre Hooks e Usare useRef per Valori Mutabili
Una volta presa confidenza, puoi combinare più custom hook in uno solo. Ad esempio, un hook useFetchWithDebounce che unisce useDebounce e useFetch. Oppure useOnlineStatus che usa useEventListener internamente.
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;
}
Un altro pattern avanzato è l'uso di useRef per tenere traccia di valori che non devono causare re-render. Ad esempio, un timer che deve essere pulito al dismount.
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]);
}
Questo hook evita che il setInterval venga reimpostato a ogni render della callback, mantenendo le performance al top.
Sponsored Protocol
Errori Comuni da Evitare con i Custom Hooks
- Non chiamare hook all'interno di condizioni o cicli: i custom hook, come tutti gli hook React, devono essere chiamati incondizionatamente e in ordine stabile.
- Ignorare la pulizia degli effetti: se il tuo hook usa
useEffectcon sottoscrizioni (event listener, interval, fetch), fornisci sempre una funzione di cleanup. - Restituire oggetti troppo grandi: se un hook restituisce molti valori, valuta di suddividerlo in hook più piccoli (principio di separazione delle responsabilità).
- Non documentare il contratto: ogni custom hook è una funzione con input e output. Documenta cosa si aspetta e cosa restituisce. Lo fai anche per il team.
Cosa Fare Adesso — Azioni Immediate
- Identifica logica ripetuta: apri il tuo codice React e cerca blocchi di
useState+useEffectche si ripetono in almeno due componenti. Estraili in un custom hook. - Crea un file
hooks/useFetch.jscon il codice sopra e sostituisci tutte le fetch manuali nei tuoi componenti. - Aggiungi un hook useDebounce alla tua search bar. Misura la differenza di chiamate API con gli strumenti sviluppatore.
- Refactoring graduale: non riscrivere tutto in una notte. Inizia da un pattern semplice (fetch, eventi) e poi estendi.
- Testa: scrivi un test per il tuo custom hook usando
@testing-library/react-hooks. Ti garantisce che il comportamento sia stabile.
Noi, di Meteora Web, abbiamo visto progetti trasformarsi da un groviglio di componenti a un codice pulito e manutenibile grazie ai custom hook. Non è solo una buona pratica — è un investimento sulla qualità del tuo software.
Per una panoramica completa su React, dai un'occhiata al nostro Pillar su React.js.