Lavorare con Refs e Portals in React

23 dicembre 2025
9 min di lettura

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 normale
function 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.

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.

Continua la lettura

Leggi il prossimo capitolo: "Context API e useReducer"

Continua a leggere