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 current nel 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.

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.