React
useRef per mutare, non per stato
useRef conserva un valore mutabile senza rirenderizzare: serve per timer, identificatori e contatori interni, non per ciò che deve comparire a schermo.
In sintesi
- useRef conserva un valore mutabile senza pianificare un render.
- Ciò che deve comparire a schermo resta stato, non ref.
- La mutazione di current va fatta in un effetto o in un gestore, mai nel corpo del render.
- La ref è per istanza: non è una cache condivisa né una variabile globale.
useRef conserva un valore mutabile per tutta la vita del componente senza chiedere un render: serve per identificatori, timer e contatori interni, non per ciò che deve comparire a schermo.
Cosa cambia rispetto allo stato
Lo stato è il valore che React disegna: cambiarlo pianifica un render. La ref è una scatola con una proprietà current: scriverla non pianifica niente, e React non la legge durante il render. Per questo la ref è il posto giusto per un id di timer, un nodo del DOM o un contatore che nessuno deve vedere.
Il caso: il campo di ricerca con debounce
import { useEffect, useRef, useState } from "react";
export function SearchBox({ onQuery }) {
const timer = useRef(null);
const requests = useRef(0);
const [value, setValue] = useState("");
useEffect(() => {
clearTimeout(timer.current);
timer.current = setTimeout(() => {
requests.current += 1;
onQuery(value);
}, 300);
return () => clearTimeout(timer.current);
}, [value, onQuery]);
return setValue(e.target.value)} />;
}
L’id del timer e il conteggio delle richieste vivono nelle ref: nessuno dei due compare nel markup, quindi nessuno dei due deve far rirenderizzare il campo a ogni tasto. Il testo digitato invece è stato, perché è quello che l’input mostra.
Mutare un oggetto senza ricrearlo
Una ref può contenere un oggetto che sopravvive ai render: una Map usata come cache, un buffer di eventi, un flag di annullamento. La mutazione in place tiene la stessa identità, quindi nessun effetto riparte solo perché il contenitore è nuovo.
const cache = useRef(new Map());
function remember(key, value) {
cache.current.set(key, value); // mutation, no render
}
Il costo è nell’inizializzazione: new Map() viene valutato a ogni render e poi scartato. Per un oggetto costoso conviene inizializzarlo alla prima lettura, con un controllo su current.
Dove la mutazione rompe
- Durante il render: scrivere
currentnel corpo del componente produce due scritture in StrictMode e valori diversi fra render concorrenti. - Come sorgente di verità: se il valore mutato deve comparire a schermo, React non lo sa e l’interfaccia resta indietro; quello è stato.
- Su un nodo del DOM: la ref va assegnata in un effetto o in una callback ref, non letta prima del mount, altrimenti
currentè nullo.
Il comando
npm create vite@latest ref-demo -- --template react-ts
cd ref-demo
npm install
npm run dev
Vite, distribuito con licenza MIT, crea il progetto e avvia il server di sviluppo; il componente si modifica in src/App.tsx senza toccare la configurazione.
Record vivi nel catalogo
Tre record mostrano ref usate per il comportamento: ai-chat-input, che misura l’altezza del campo, use-image-upload, un hook costruito su una ref, e la categoria input con i campi pubblicati.
La limitazione
Una ref non notifica nessuno: chi la legge deve sapere quando farlo, e due componenti non possono sincronizzarsi su di essa. La mutazione è anche invisibile alla timeline di React DevTools, quindi un valore sbagliato in current si trova solo leggendo il codice.
Dove continuare
Il confine fra ref, DOM e stato è in ref, DOM e stato; l’ordine delle chiamate è in regole degli hook; il doppio avvio degli effetti in sviluppo è in useEffect due volte; il confronto fra i framework è in Vue 3 vs React.
Domande frequenti
Quando uso useRef invece di useState?
Quando il valore non compare nel markup: id di timer, nodi, contatori interni. Se il render deve mostrarlo, serve useState.
Perché useRef non fa rirenderizzare?
Perché React non legge current durante il render e non registra le scritture: la scatola resta fuori dal ciclo di aggiornamento.
Posso mutare un oggetto dentro una ref?
Sì, fuori dal render. La mutazione in place è il motivo per cui la ref esiste, ma nel corpo del componente rompe StrictMode e il rendering concorrente.
Perché current è null al primo render?
Perché la ref viene collegata al nodo solo dopo il mount. Leggila in un effetto o in una callback ref, non durante il render.
useRef sostituisce una variabile globale?
No. Ogni istanza del componente ha la sua ref: non condivide dati fra componenti e non sopravvive allo smontaggio.