Durante lo sviluppo, spesso abbiamo bisogno di interazione diretta con gli elementi del DOM. In questi casi, React ci offre un meccanismo chiamato refs, che consente l’accesso agli elementi dopo che sono stati renderizzati. Più comunemente, utilizziamo i refs oggetto standard tramite useRef
(chiamiamoli così), ma c’è un altro approccio conosciuto come callback refs. Questo metodo offre ulteriore flessibilità e controllo sul ciclo di vita degli elementi, permettendoci di eseguire azioni specifiche in momenti precisi quando gli elementi vengono aggiunti o rimossi dal DOM.
In questo articolo, voglio spiegare cosa sono i callback refs e come funzionano, discutere i problemi che potresti incontrare e mostrare esempi del loro utilizzo.
Cosa Sono i Callback Refs e Come Funzionano?
I callback refs ti danno un controllo più granulare sull’attacco dei ref rispetto ai refs oggetto. Vediamo come funzionano nella pratica:
- Montaggio. Quando un elemento viene montato nel DOM, React chiama la funzione ref con l’elemento DOM stesso. Questo ti consente di eseguire azioni con l’elemento immediatamente dopo che appare sulla pagina.
- Smontaggio. Quando un elemento viene smontato, React chiama la funzione ref con
null
. Questo ti offre l’opportunità di pulire o annullare eventuali azioni associate a quell’elemento.
Esempio: Monitoraggio del Montaggio e Smontaggio
import React, { useCallback, useState } from 'react';
function MountUnmountTracker() {
const [isVisible, setIsVisible] = useState(false);
const handleRef = useCallback((node: HTMLDivElement | null) => {
if (node) {
console.log('Element mounted:', node);
} else {
console.log('Element unmounted');
}
}, []);
return (
<div>
<button onClick={() => setIsVisible((prev) => !prev)}>
{isVisible ? 'Hide' : 'Show'} element
</button>
{isVisible && <div ref={handleRef}>Tracked element</div>}
</div>
);
}
export default MountUnmountTracker;
Ogni volta che cambiamo la visibilità dell’elemento, la funzione handleRef
viene chiamata con il nodo o null
, permettendoci di tracciare il momento in cui l’elemento viene allegato o staccato.
Problemi Comuni e Soluzioni
Problema: Invocazioni Ripetute del Callback Ref
Un problema frequente nell’uso dei callback refs è la creazione ripetuta della funzione ref ad ogni ri-rendering del componente. A causa di ciò, React pensa che sia un nuovo ref, chiama quello vecchio con null
(pulendolo) e poi inizializza quello nuovo, anche se il nostro elemento o componente non è effettivamente cambiato. Questo può portare a effetti collaterali indesiderati.
Esempio del Problema
Considera un componente Basic
che ha un pulsante per cambiare la visibilità di un div
con un callback ref, più un altro pulsante per forzare un ri-rendering del componente:
import React, { useState, useReducer } from 'react';
function Basic() {
const [showDiv, setShowDiv] = useState(false);
const [, forceRerender] = useReducer((v) => v + 1, 0);
const toggleDiv = () => setShowDiv((prev) => !prev);
const refCallback = (node: HTMLDivElement | null) => {
console.log('div', node);
};
return (
<div>
<button onClick={toggleDiv}>Toggle Div</button>
<button onClick={forceRerender}>Rerender</button>
{showDiv && <div ref={refCallback}>Example div</div>}
</div>
);
}
export default Basic;
Ogni volta che si fa clic sul pulsante Rerender, il componente si ri-renderizza, creando una nuova funzione refCallback
. Di conseguenza, React chiama la vecchia refCallback(null)
e poi la nuova refCallback(node)
, anche se il nostro elemento con il ref non è cambiato. Nella console, vedrai div null
e poi div [node]
a turno, ripetutamente. Chiaramente, di solito vogliamo evitare chiamate non necessarie come queste.
Soluzione: Memorizzare il Callback Ref Con useCallback
Evitare questo è piuttosto semplice: basta utilizzare useCallback
per memorizzare la funzione. In questo modo, la funzione rimane invariata attraverso i ri-render, a meno che le sue dipendenze cambino.
import React, { useState, useCallback, useReducer } from 'react';
function Basic() {
const [showDiv, setShowDiv] = useState(false);
const [, forceRerender] = useReducer((v) => v + 1, 0);
const toggleDiv = () => setShowDiv((prev) => !prev);
const refCallback = useCallback((node: HTMLDivElement | null) => {
console.log('div', node);
}, []);
return (
<div>
<button onClick={toggleDiv}>Toggle Div</button>
<button onClick={forceRerender}>Rerender</button>
{showDiv && <div ref={refCallback}>Example div</div>}
</div>
);
}
export default Basic;
Ora, refCallback
viene creato solo una volta, al rendering iniziale. Non attiverà chiamate aggiuntive nei successivi re-rendering, prevenendo callback non necessari e migliorando le prestazioni.
L’Ordine dei Callback Refs, useLayoutEffect e useEffect
Prima di parlare di come utilizzare i callback refs nel tuo codice per risolvere problemi specifici, comprendiamo come i callback refs interagiscono con i hook useEffect
e useLayoutEffect
in modo da poter organizzare correttamente l’inizializzazione e la pulizia delle risorse.
Ordine di Esecuzione
- callback ref – chiamato immediatamente dopo il rendering degli elementi DOM, prima che vengano eseguiti gli hook di effetto
- useLayoutEffect – si esegue dopo tutte le mutazioni del DOM ma prima che il browser disegni
- useEffect – si esegue dopo che il componente ha terminato il rendering sullo schermo
import React, { useEffect, useLayoutEffect, useCallback } from 'react';
function WhenCalled() {
const refCallback = useCallback((node: HTMLDivElement | null) => {
if (node) {
console.log('Callback ref called for div:', node);
} else {
console.log('Callback ref detached div');
}
}, []);
useLayoutEffect(() => {
console.log('useLayoutEffect called');
}, []);
useEffect(() => {
console.log('useEffect called');
}, []);
return (
<div>
<div ref={refCallback}>Element to watch</div>
</div>
);
}
export default WhenCalled;
Output della Console
-
Callback ref chiamato per div: [elemento div]
-
useLayoutEffect chiamato
-
useEffect chiamato
Questa sequenza ci dice che i callback refs vengono attivati prima degli hook come useLayoutEffect
e useEffect
, il che è essenziale tenere a mente quando scrivi la tua logica.
Quali Problemi Risolvono i Callback Refs nel Codice?
Per prima cosa, riproduciamo un problema tipicamente incontrato con i riferimenti agli oggetti regolari in modo da poterlo poi risolvere con i callback refs.
import { useCallback, useEffect, useRef, useState } from 'react';
interface ResizeObserverOptions {
elemRef: React.RefObject<HTMLElement>;
onResize: ResizeObserverCallback;
}
function useResizeObserver({ elemRef, onResize }: ResizeObserverOptions) {
useEffect(() => {
const element = elemRef.current;
if (!element) {
return;
}
const resizeObserver = new ResizeObserver(onResize);
resizeObserver.observe(element);
return () => {
resizeObserver.unobserve(element);
};
}, [onResize, elemRef]);
}
export function UsageDom() {
const [bool, setBool] = useState(false);
const elemRef = useRef<HTMLDivElement>(null);
const handleResize = useCallback((entries: ResizeObserverEntry[]) => {
console.log('resize', entries);
}, []);
useResizeObserver({ elemRef, onResize: handleResize });
const renderTestText = () => {
if (bool) {
return <p ref={elemRef}>Test text</p>;
}
return <div ref={elemRef}>Test div</div>;
};
return (
<div style={{ width: '100%', textAlign: 'center' }}>
<button onClick={() => setBool((v) => !v)}>Toggle</button>
{renderTestText()}
</div>
);
}
Non entreremo in ogni dettaglio qui. In breve, stiamo tracciando la dimensione del nostro elemento div
o p
tramite un ResizeObserver
. Inizialmente, tutto funziona bene: al montaggio, possiamo ottenere la dimensione dell’elemento e le ridimensionamenti vengono anche segnalati nella console.
Il vero problema inizia quando cambiamo lo stato per passare all’elemento che stiamo osservando. Quando cambiamo lo stato e quindi sostituiamo l’elemento tracciato, il nostro ResizeObserver
non funziona più correttamente. Continua a osservare il primo elemento, che è già stato rimosso dal DOM! Anche tornare all’elemento originale non aiuta perché l’iscrizione al nuovo elemento non si attacca correttamente.
Nota: La seguente soluzione è più rappresentativa di ciò che potresti scrivere in un contesto di libreria, necessitando di codice universale. In un progetto reale, potresti risolverlo tramite una combinazione di flag, effetti, ecc. Ma nel codice della libreria, non si ha conoscenza del componente specifico o del suo stato. Questo è esattamente lo scenario in cui i riferimenti di callback possono aiutarci.
import { useCallback, useRef, useState } from 'react';
function useResizeObserver(onResize: ResizeObserverCallback) {
const roRef = useRef<ResizeObserver | null>(null);
const attachResizeObserver = useCallback(
(element: HTMLElement) => {
const resizeObserver = new ResizeObserver(onResize);
resizeObserver.observe(element);
roRef.current = resizeObserver;
},
[onResize]
);
const detachResizeObserver = useCallback(() => {
roRef.current?.disconnect();
}, []);
const refCb = useCallback(
(element: HTMLElement | null) => {
if (element) {
attachResizeObserver(element);
} else {
detachResizeObserver();
}
},
[attachResizeObserver, detachResizeObserver]
);
return refCb;
}
export default function App() {
const [bool, setBool] = useState(false);
const handleResize = useCallback((entries: ResizeObserverEntry[]) => {
console.log('resize', entries);
}, []);
const resizeRef = useResizeObserver(handleResize);
const renderTestText = () => {
if (bool) {
return <p ref={resizeRef}>Test text</p>;
}
return <div ref={resizeRef}>Test div</div>;
};
return (
<div style={{ width: '100%', textAlign: 'center' }}>
<button onClick={() => setBool((v) => !v)}>Toggle</button>
{renderTestText()}
</div>
);
}
Come puoi vedere, abbiamo riscritto il nostro hook useResizeObserver
per utilizzare un riferimento di callback. Passiamo semplicemente un callback (memoizzato) che dovrebbe essere attivato al ridimensionamento e, non importa quante volte cambi elementi, il nostro callback di ridimensionamento funziona ancora. Questo perché l’osservatore si attacca ai nuovi elementi e si stacca dai vecchi al momento esatto desiderato, grazie al riferimento di callback.
Il principale vantaggio qui è che lo sviluppatore che utilizza il nostro hook non deve più preoccuparsi della logica di aggiunta/rimozione degli osservatori sotto il cofano: abbiamo incapsulato quella logica all’interno dell’hook. Lo sviluppatore deve solo passare una callback al nostro hook e attaccare il riferimento restituito ai loro elementi.
Combinare Più Riferimenti In Uno
Ecco un altro scenario in cui i riferimenti di callback vengono in soccorso:
import { useEffect, useRef } from 'react';
import { forwardRef, useCallback } from 'react';
interface InputProps {
value?: string;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
}
const Input = forwardRef(function Input(
props: InputProps,
ref: React.ForwardedRef<HTMLInputElement>
) {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (!inputRef.current) {
return;
}
console.log(inputRef.current.getBoundingClientRect());
}, []);
return <input {...props} ref={ref} />;
});
export function UsageWithoutCombine() {
const inputRef = useRef<HTMLInputElement | null>(null);
const focus = () => {
inputRef.current?.focus();
};
return (
<div>
<Input ref={inputRef} />
<button onClick={focus}>Focus</button>
</div>
);
}
Nel codice sopra, abbiamo un semplice componente di input su cui impostiamo un riferimento, ottenendolo dalle props utilizzando forwardRef
. Ma come possiamo utilizzare inputRef
all’interno del componente Input
se abbiamo anche bisogno del riferimento dall’esterno per qualcosa come il focus? Forse vogliamo fare qualcos’altro nel componente di input stesso, come getBoundingClientRect
. Sostituire il riferimento delle props con il nostro riferimento interno significa che il focus dall’esterno non funzionerà più. Quindi, come possiamo combinare questi due riferimenti?
Ecco dove i riferimenti di callback aiutano di nuovo:
import { useEffect, useRef } from 'react';
import { forwardRef, useCallback } from 'react';
type RefItem<T> =
| ((element: T | null) => void)
| React.MutableRefObject<T | null>
| null
| undefined;
function useCombinedRef<T>(...refs: RefItem<T>[]) {
const refCb = useCallback((element: T | null) => {
refs.forEach((ref) => {
if (!ref) {
return;
}
if (typeof ref === 'function') {
ref(element);
} else {
ref.current = element;
}
});
}, refs);
return refCb;
}
interface InputProps {
value?: string;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
}
const Input = forwardRef(function Input(
props: InputProps,
ref: React.ForwardedRef<HTMLInputElement>
) {
const inputRef = useRef<HTMLInputElement>(null);
const combinedInputRef = useCombinedRef(ref, inputRef);
useEffect(() => {
if (!inputRef.current) {
return;
}
console.log(inputRef.current.getBoundingClientRect());
}, []);
return <input {...props} ref={combinedInputRef} />;
});
export function UsageWithCombine() {
const inputRef = useRef<HTMLInputElement | null>(null);
const focus = () => {
inputRef.current?.focus();
};
return (
<div>
<Input ref={inputRef} />
<button onClick={focus}>Focus</button>
</div>
);
}
Spiegazione
Abbiamo implementato un hook useCombinedRef
in cui uno sviluppatore può passare riferimenti standard, riferimenti di callback e eventualmente null
o undefined
. L’hook stesso è semplicemente un useCallback
che cicla sull’array dei riferimenti. Se l’argomento è null
, lo ignoriamo, ma se il riferimento è una funzione, lo chiamiamo con l’elemento; se è un oggetto riferimento standard, impostiamo ref.current
sull’elemento.
In questo modo, uniamo più riferimenti in uno. Nell’esempio sopra, sia getBoundingClientRect
all’interno del componente Input
, sia la chiamata esterna al focus funzioneranno correttamente.
Qual è la novità in React 19 riguardo ai Riferimenti di Callback?
Pulizia Automatica
React ora gestisce automaticamente la pulizia dei riferimenti di callback quando gli elementi vengono smontati, semplificando la gestione delle risorse. Ecco un esempio che il team di React mostra nella loro documentazione:
{
// riferimento creato
// NUOVO: restituisci una funzione di pulizia per resettare
// il riferimento quando l'elemento viene rimosso dal DOM.
return () => {
// pulizia del riferimento
};
}}
/>
Puoi leggere maggiori dettagli al riguardo nel post ufficiale del blog, dove si menziona che presto, la pulizia dei riferimenti tramite null
potrebbe essere deprecata, lasciando un’unica modalità standardizzata per pulire i riferimenti.
Scegliere tra Riferimenti Normali e Riferimenti di Callback
- Utilizza riferimenti standard (
useRef
) quando hai semplicemente bisogno di un accesso semplice a un elemento DOM o desideri preservare un valore tra i rendering senza azioni aggiuntive durante l’attacco o il distacco. - Utilizza riferimenti di callback quando richiedi un controllo più granulare sul ciclo di vita dell’elemento, quando stai scrivendo codice universale (ad esempio, nella tua libreria o pacchetto), o quando hai bisogno di gestire più riferimenti insieme.
Conclusione
I callback refs in React sono uno strumento potente che offre agli sviluppatori maggiore flessibilità e controllo quando si lavora con elementi DOM. Nella maggior parte dei casi, i riferimenti agli oggetti standard tramite useRef
sono sufficienti per i compiti quotidiani, ma i callback refs possono essere utili per scenari più complessi come quelli di cui abbiamo parlato sopra.
Source:
https://dzone.com/articles/react-callback-refs-guide