Introduzione
I refs permettono di ottenere accesso diretto agli elementi DOM e di gestire valori che persistono tra i render senza causare re-render. I portals permettono di renderizzare JSX in punti diversi del DOM rispetto a dove viene utilizzato nel codice.
Cosa sono i Refs
Un ref in React è un valore speciale gestito da React in modo particolare. A differenza dello state, che causa il re-render del componente quando viene aggiornato, i refs permettono di memorizzare valori che persistono tra i render senza causare aggiornamenti dell’interfaccia.
I refs vengono creati utilizzando l’hook useRef, che restituisce un oggetto con una proprietà current che contiene il valore effettivo del ref.
Accesso agli Elementi DOM
Il caso d’uso più comune per i refs è ottenere un accesso diretto agli elementi HTML del DOM. Questo è utile quando si ha bisogno di leggere o manipolare direttamente un elemento, ad esempio per leggere il valore di un input senza gestirlo con lo state.
Esempio: Leggere il valore di un input con refs
Invece di gestire ogni cambiamento dell’input con onChange e useState, si può usare un ref per leggere il valore solo quando necessario:
import { useRef, useState } from 'react';
function Player() { // Creazione del ref per l'elemento input const playerNameRef = useRef(); const [enteredPlayerName, setEnteredPlayerName] = useState('');
function handleClick() { // Accesso al valore dell'input tramite ref // current contiene l'elemento HTML effettivo const enteredValue = playerNameRef.current.value; setEnteredPlayerName(enteredValue); }
return ( <section> <h2>Welcome {enteredPlayerName || 'unknown entity'}!</h2> <p> <input type="text" ref={playerNameRef} // Collegamento del ref all'elemento /> <button onClick={handleClick}>Set Name</button> </p> </section> );}Il ref viene collegato all’elemento solo dopo il primo render. Nel primo ciclo di rendering, playerNameRef.current sarà undefined.
Manipolazione Diretta del DOM
I refs permettono anche di manipolare direttamente gli elementi DOM, ad esempio per resettare un input o chiamare metodi nativi del browser come click() su un elemento nascosto.
Esempio: Reset di un input e trigger di click
function Player() { const playerNameRef = useRef();
function handleClick() { const enteredValue = playerNameRef.current.value; setEnteredPlayerName(enteredValue);
// Reset dell'input dopo aver letto il valore playerNameRef.current.value = ''; }
return ( <input type="text" ref={playerNameRef} /> );}Esempio con file picker nascosto:
function ImageUploader() { const filePickerRef = useRef();
function handlePickImage() { // Trigger del click sull'input file nascosto filePickerRef.current.click(); }
return ( <> <input type="file" accept="image/*" ref={filePickerRef} style={{ display: 'none' }} /> <button onClick={handlePickImage}>Pick Image</button> </> );}Questa è manipolazione imperativa del DOM, accettabile per casi semplici come reset di input o trigger di eventi.
Refs vs State
La differenza fondamentale è che lo state causa il re-render quando viene aggiornato, mentre i refs no. Lo state deve essere usato per valori che devono essere riflessi nell’interfaccia. I refs sono per valori che non influenzano direttamente l’UI o per accedere a elementi DOM.
Esempio: Perché non usare solo refs per tutto
Se si prova a usare solo un ref per mostrare il valore nell’interfaccia:
function Player() { const playerNameRef = useRef();
function handleClick() { // Aggiornamento del ref // Ma questo NON causa un re-render! }
return ( <h2>Welcome {playerNameRef.current?.value || 'unknown entity'}!</h2> );}Se si usa solo un ref, playerNameRef.current è undefined nel primo render e l’interfaccia non si aggiorna quando il valore cambia. Serve lo state per aggiornare l’UI:
function Player() { const playerNameRef = useRef(); const [enteredPlayerName, setEnteredPlayerName] = useState('');
function handleClick() { // Leggo il valore dal ref const value = playerNameRef.current.value; // Aggiorno lo state per triggerare il re-render setEnteredPlayerName(value); }
return ( <h2>Welcome {enteredPlayerName || 'unknown entity'}!</h2> );}Gestire Valori Non Dichiarativi con Refs
Un altro caso d’uso importante per i refs è gestire valori che non devono essere dichiarativi, come i timer ID restituiti da setTimeout o setInterval. Questi valori devono persistere tra i render ma non devono causare aggiornamenti dell’interfaccia.
Gestione di Timer con Refs
Quando si gestiscono timer in React, è necessario memorizzare l’ID del timer per poterlo cancellare successivamente. Usare una variabile normale non funziona perché viene ricreata ad ogni render.
Problema: Timer con variabile normale
function TimerChallenge({ targetTime }) { let timer; // ❌ Problema: viene ricreata ad ogni render
function handleStart() { timer = setTimeout(() => { console.log('Timer expired!'); }, targetTime * 1000); }
function handleStop() { clearTimeout(timer); // ❌ timer potrebbe essere undefined o riferirsi a un timer diverso }}Quando lo state cambia, il componente viene re-renderizzato e la variabile timer viene ricreata, perdendo il riferimento al timer precedente.
Soluzione: Timer con ref
import { useRef, useState } from 'react';
function TimerChallenge({ targetTime }) { // Ref per memorizzare l'ID del timer const timerRef = useRef(); const [timerStarted, setTimerStarted] = useState(false);
function handleStart() { // Memorizzazione dell'ID del timer nel ref timerRef.current = setTimeout(() => { setTimerExpired(true); }, targetTime * 1000);
setTimerStarted(true); }
function handleStop() { // Accesso al timer tramite ref e cancellazione clearTimeout(timerRef.current); setTimerStarted(false); }
return ( <section className="challenge"> <h2>{title}</h2> <button onClick={timerStarted ? handleStop : handleStart}> {timerStarted ? 'Stop Challenge' : 'Start Challenge'} </button> </section> );}Il ref persiste tra i render senza causare re-render, quindi il valore in timerRef.current rimane accessibile anche dopo re-render multipli. Lo stesso principio si applica a setInterval con clearInterval.
Gestione di Timer con setInterval
Quando si ha bisogno di aggiornare il tempo rimanente continuamente, setInterval è più appropriato di setTimeout:
Esempio: Timer con tempo rimanente
function TimerChallenge({ targetTime }) { const timerRef = useRef(); const [timeRemaining, setTimeRemaining] = useState(targetTime * 1000);
function handleStart() { // setInterval esegue la funzione ogni 10ms timerRef.current = setInterval(() => { setTimeRemaining((prevTime) => { const newTime = prevTime - 10;
// Se il tempo è scaduto, fermiamo l'intervallo if (newTime <= 0) { clearInterval(timerRef.current); return 0; }
return newTime; }); }, 10); // Aggiornamento ogni 10 millisecondi }
function handleStop() { // Cancellazione dell'intervallo clearInterval(timerRef.current); }
// Cleanup quando il componente viene smontato useEffect(() => { return () => { if (timerRef.current) { clearInterval(timerRef.current); } }; }, []);
const isActive = timeRemaining > 0 && timeRemaining < targetTime * 1000;
return ( <section> <p>Time remaining: {(timeRemaining / 1000).toFixed(2)}s</p> <button onClick={isActive ? handleStop : handleStart}> {isActive ? 'Stop' : 'Start'} </button> </section> );}setInterval restituisce un ID che va memorizzato per poterlo cancellare con clearInterval. È importante fare cleanup quando il componente viene smontato.
Esporre API dei Componenti con Refs
Quando si crea un componente che deve essere controllato dall’esterno, si può esporre un’API utilizzando forwardRef e useImperativeHandle. Questo permette di chiamare metodi del componente da componenti genitori senza dover conoscere i dettagli interni dell’implementazione.
forwardRef: Passare Refs ai Componenti Custom
Per default, i componenti custom non possono ricevere il prop ref. Per permettere questo, si deve avvolgere il componente con forwardRef.
Esempio: Componente Input che accetta ref
Senza forwardRef (React 19+):
function Input({ label, ...props }) { return ( <p className="control"> <label>{label}</label> <input {...props} /> </p> );}
// In React 19+, si può accettare ref come prop normalefunction Input({ label, ref, ...props }) { return ( <p className="control"> <label>{label}</label> <input ref={ref} {...props} /> </p> );}Con forwardRef (necessario per React < 19):
import { forwardRef } from 'react';
const Input = forwardRef(function Input({ label, ...props }, ref) { return ( <p className="control"> <label>{label}</label> <input ref={ref} {...props} /> </p> );});
export default Input;Utilizzo:
function App() { const nameRef = useRef(); const emailRef = useRef();
function handleSave() { const name = nameRef.current.value; const email = emailRef.current.value; console.log({ name, email }); }
return ( <> <Input type="text" label="Name" ref={nameRef} /> <Input type="email" label="Email" ref={emailRef} /> <button onClick={handleSave}>Save</button> </> );}useImperativeHandle: Esporre Metodi Personalizzati
useImperativeHandle permette di definire quali metodi e proprietà del componente devono essere accessibili dall’esterno tramite ref. Questo crea un’API stabile che non dipende dai dettagli interni dell’implementazione.
Esempio: Modal con API esposta
import { forwardRef, useImperativeHandle, useRef } from 'react';
const ResultModal = forwardRef(function ResultModal({ targetTime, timeRemaining, onReset }, ref) { // Ref interno per l'elemento dialog const dialogRef = useRef();
// Esposizione dell'API del componente useImperativeHandle(ref, () => { return { // Metodo che può essere chiamato dall'esterno open() { dialogRef.current.showModal(); } }; });
return ( <dialog ref={dialogRef} className="result-modal" onClose={onReset}> <h2> {timeRemaining <= 0 ? 'You lost!' : 'Your score'} </h2> <p> The target time was <strong>{targetTime}s</strong> </p> {timeRemaining > 0 && ( <p> You stopped the timer with <strong>{(timeRemaining / 1000).toFixed(2)}s</strong> left. </p> )} <form method="dialog"> <button>Close</button> </form> </dialog> );});
export default ResultModal;Utilizzo nel componente genitore:
function TimerChallenge({ targetTime }) { const dialogRef = useRef();
function handleStop() { clearInterval(timerRef.current); // Chiamata al metodo esposto dal componente Modal dialogRef.current.open(); }
return ( <> <section className="challenge"> {/* ... */} </section> <ResultModal ref={dialogRef} targetTime={targetTime} timeRemaining={timeRemaining} onReset={handleReset} /> </> );}Il componente genitore non deve conoscere i dettagli interni. Se si cambia l’implementazione, l’API rimane stabile.
Esempio: Form con metodo clear() esposto
import { forwardRef, useImperativeHandle, useRef } from 'react';
const Form = forwardRef(function Form(props, ref) { const formRef = useRef();
useImperativeHandle(ref, () => { return { clear() { // Chiamata al metodo reset() nativo del form formRef.current.reset(); } }; });
return ( <form ref={formRef}> <p> <label>Name</label> <input type="text" /> </p> <p> <label>Email</label> <input type="email" /> </p> <button type="submit">Save</button> </form> );});
export default Form;Utilizzo:
function App() { const formRef = useRef();
function handleRestart() { // Reset del form dall'esterno formRef.current.clear(); }
return ( <> <button onClick={handleRestart}>Restart</button> <Form ref={formRef} /> </> );}Portals: Renderizzare in Punti Diversi del DOM
I portals permettono di renderizzare un componente in un punto diverso del DOM rispetto a dove viene utilizzato nel codice JSX. Questo è particolarmente utile per modali, tooltip, e altri elementi che devono apparire sopra il resto del contenuto.
Perché Usare i Portals
I portals permettono di renderizzare modali e overlay direttamente nel <body> o in un contenitore dedicato, evitando problemi di z-index e overflow e migliorando l’accessibilità.
Problema senza Portal
Senza portal, una modale viene renderizzata dove viene utilizzata nel JSX:
function TimerChallenge() { return ( <section className="challenge"> <h2>Challenge Title</h2> {/* La modale è annidata qui dentro */} <ResultModal /> </section> );}Struttura DOM risultante:
<div id="root"> <div id="challenges"> <section class="challenge"> <h2>Challenge Title</h2> <!-- Modale annidata qui --> <dialog class="result-modal">...</dialog> </section> </div></div>La modale è annidata profondamente e potrebbe essere nascosta da overflow o z-index di elementi genitori.
Utilizzo di createPortal
createPortal è una funzione di react-dom che permette di “teletrasportare” il JSX in un punto diverso del DOM.
Esempio: Modal con Portal
Struttura HTML:
<!DOCTYPE html><html> <body> <div id="root"></div> <!-- Contenitore dedicato per le modali --> <div id="modal"></div> </body></html>Componente Modal con Portal:
import { createPortal } from 'react-dom';
function ResultModal({ targetTime, timeRemaining, onReset }) { const dialogRef = useRef();
return createPortal( <dialog ref={dialogRef} className="result-modal" onClose={onReset}> <h2> {timeRemaining <= 0 ? 'You lost!' : 'Your score'} </h2> <p> The target time was <strong>{targetTime}s</strong> </p> {timeRemaining > 0 && ( <p> You stopped the timer with <strong>{(timeRemaining / 1000).toFixed(2)}s</strong> left. </p> )} <form method="dialog"> <button>Close</button> </form> </dialog>, // Secondo argomento: dove renderizzare nel DOM document.getElementById('modal') );}Utilizzo (rimane invariato):
function TimerChallenge() { return ( <section className="challenge"> <h2>Challenge Title</h2> {/* Usato qui nel JSX */} <ResultModal /> </section> );}Struttura DOM risultante:
<div id="root"> <div id="challenges"> <section class="challenge"> <h2>Challenge Title</h2> <!-- Nessuna modale qui --> </section> </div></div>
<!-- Modale renderizzata qui invece --><div id="modal"> <dialog class="result-modal">...</dialog></div>La modale è renderizzata vicino alla root del DOM, evitando problemi di z-index e overflow, mentre il codice JSX rimane organizzato logicamente.
Esempio: Toast con Portal nel body
import { createPortal } from 'react-dom';
function Toast({ message }) { return createPortal( <aside className="toast"> <p>{message}</p> </aside>, document.querySelector('body') );}
function App() { const [toastVisible, setToastVisible] = useState(false);
function handleEnrol() { setToastVisible(true);
setTimeout(() => { setToastVisible(false); }, 3000); }
return ( <div id="app"> {toastVisible && <Toast message="Enrolled successfully!" />} <article> <h2>React Course</h2> <button onClick={handleEnrol}>Enrol</button> </article> </div> );}Anche se il componente Toast viene utilizzato dentro <div id="app">, viene renderizzato direttamente nel <body> grazie al portal.
Importante: react-dom vs react
createPortal viene importato da react-dom e non da react perché:
- react: Contiene funzionalità che funzionano in tutti gli ambienti (web, React Native, etc.)
- react-dom: Contiene funzionalità specifiche per il DOM del browser
I portals sono specifici per il rendering nel DOM del browser, quindi appartengono a react-dom.
Riepilogo
I refs sono valori che persistono tra i render senza causare re-render. Si usano per accedere direttamente agli elementi DOM, gestire timer ID (setTimeout/setInterval), ed esporre API dei componenti con forwardRef e useImperativeHandle.
I portals permettono di renderizzare JSX in punti diversi del DOM usando createPortal da react-dom. Sono utili per modali, tooltip e toast che devono apparire sopra il resto del contenuto, evitando problemi di z-index e overflow.
Il ref viene collegato all’elemento dopo il primo render. Per timer e intervalli, è importante fare cleanup quando il componente viene smontato.