Menu

useSyncExternalStore in React: iscriversi a dati esterni

useSyncExternalStore iscrive un componente a dati che vivono al di fuori di React, come un piccolo modulo store o un'API del browser come navigator.onLine, e lo renderizza di nuovo ogni volta che quei dati cambiano. Impara subscribe e getSnapshot, perché lo snapshot va messo in cache, e getServerSnapshot.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

useSyncExternalStore iscrive un componente a dati che vivono al di fuori di React e lo renderizza di nuovo ogni volta che quei dati cambiano. Gli dai due funzioni: subscribe, che dice a React come ascoltare i cambiamenti, e getSnapshot, che restituisce il valore attuale.

I due componenti Display non condividono props né context, eppure si aggiornano insieme, perché entrambi sono iscritti allo stesso store. Il pulsante chiama una semplice funzione, non un setter di React. Aggiungi un terzo <Display name="Sidebar" /> e si unisce agli altri senza alcuna altra modifica.

La sintassi

const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);
  • subscribe(callback) inizia ad ascoltare, chiama callback ogni volta che i dati potrebbero essere cambiati e restituisce una funzione per annullare l'iscrizione. React la chiama dopo il montaggio del componente e chiama la funzione restituita allo smontaggio.
  • getSnapshot() restituisce il valore attuale. React la chiama a ogni rendering e dopo ogni notifica, poi confronta il risultato con l'ultimo usando Object.is. Stesso valore, nessun rendering.
  • getServerSnapshot() (facoltativa) restituisce il valore da usare sul server e durante l'hydration.

Definisci subscribe fuori dal componente, oppure mantienila stabile con useCallback. Se passi una nuova funzione subscribe a ogni rendering, React annulla l'iscrizione e si iscrive di nuovo ogni volta.

API del browser come store

Qualsiasi cosa che abbia un valore attuale e generi un evento quando il valore cambia si adatta a questa forma. navigator.onLine con gli eventi online e offline è il caso classico.

Disattiva la rete (o passa a Offline negli strumenti per sviluppatori del browser) e il testo cambia senza ricaricare. Il terzo argomento dice "considera la connessione attiva" durante il rendering sul server, dove non c'è alcun navigator. Racchiudere la chiamata all'hook in useOnlineStatus lo rende un hook personalizzato che qualsiasi componente può usare.

La larghezza della finestra funziona allo stesso modo:

Aggiungi qualche copia e ridimensiona la finestra: ogni copia mostra lo stesso numero nello stesso momento. Ogni copia ha il proprio listener, e ognuna legge la larghezza durante il rendering, quindi nessuna è mai indietro di un frame.

getSnapshot deve restituire un valore in cache

React chiama getSnapshot spesso e confronta i risultati per riferimento. Una funzione che costruisce un nuovo oggetto o array a ogni chiamata sembra sempre un cambiamento:

// Broken: a new object on every call
function getSnapshot() {
    return { count: store.count, user: store.user };
}

// Also broken: filter returns a new array every time
function getSnapshot() {
    return store.todos.filter((t) => !t.done);
}

React esegue il rendering, chiama getSnapshot, ottiene un valore "diverso", esegue di nuovo il rendering, e così via, finché non si ferma con "Maximum update depth exceeded". In sviluppo React prima registra anche "The result of getSnapshot should be cached to avoid an infinite loop". Una build di produzione, come l'anteprima qui, salta quell'avviso e segnala solo l'errore finale come breve codice (Minified React error #185).

La soluzione è mantenere i dati immutabili nello store: sostituisci l'oggetto quando cambia, e restituisci il riferimento memorizzato così com'è.

getSnapshot restituisce lo stesso oggetto state finché add non lo sostituisce, quindi il componente si renderizza una volta per ogni modifica. Il filtraggio avviene nel componente, dopo che lo snapshot è stato letto, il che è sicuro. Per filtrare invece dentro lo store, calcola l'array filtrato quando i dati cambiano e memorizzalo, così getSnapshot può restituire la copia memorizzata.

getServerSnapshot e hydration

Sul server non ci sono finestra, navigator né sottoscrizioni. Il terzo argomento dice a React cosa renderizzare lì:

const width = useSyncExternalStore(
    subscribe,
    () => window.innerWidth, // in the browser
    () => 1024 // on the server, and during hydration
);

React usa getServerSnapshot anche per il primo rendering nel browser quando idrata l'HTML del server, così i due coincidono. Subito dopo l'hydration legge getSnapshot e, se il valore reale è diverso, esegue un nuovo render con esso. Senza il terzo argomento, il rendering sul server genera "Missing getServerSnapshot, which is required for server-rendered content. Will revert to client rendering.". Se sopra il componente c'è un confine <Suspense>, il server invia il fallback di quel confine e il browser renderizza invece il suo contenuto; senza alcun confine, il rendering sul server fallisce.

useSyncExternalStore vs useEffect e useState

Puoi iscriverti con un effetto:

function useOnlineStatus() {
    const [online, setOnline] = useState(true);
    useEffect(() => {
        const update = () => setOnline(navigator.onLine);
        update();
        window.addEventListener('online', update);
        window.addEventListener('offline', update);
        return () => {
            window.removeEventListener('online', update);
            window.removeEventListener('offline', update);
        };
    }, []);
    return online;
}

Funziona, con due punti deboli. Il primo rendering mostra sempre la stima iniziale, e il valore reale arriva un rendering dopo, quando l'effetto viene eseguito. E con il rendering concorrente (durante una transizione, per esempio), React può mettere in pausa un rendering a metà; se lo store cambia durante la pausa, i componenti renderizzati prima e dopo possono mostrare valori diversi. Questa incoerenza si chiama tearing. useSyncExternalStore legge il valore durante il rendering e costringe React a rifare il rendering in modo sincrono se lo store è cambiato, quindi ogni componente vede lo stesso valore.

Usalo quando i dati vivono fuori da React: il tuo modulo store, un'API del browser, una libreria di terze parti. La maggior parte delle librerie di stato (Redux, Zustand e altre) lo chiama per te dentro i loro hook. Per i dati che appartengono ai tuoi componenti, useState, useReducer e il context restano gli strumenti giusti.

Domande frequenti

A cosa serve useSyncExternalStore?

A leggere dati che React non possiede e che possono cambiare da soli: uno store scritto fuori da React, una libreria di stato di terze parti, o un valore del browser come navigator.onLine o la larghezza della finestra. Il componente esegue un nuovo render ogni volta che lo store comunica a React di essere cambiato.

Cosa fanno subscribe e getSnapshot?

subscribe(callback) inizia ad ascoltare lo store, chiama callback a ogni modifica e restituisce una funzione che smette di ascoltare. getSnapshot() restituisce il valore attuale. React chiama getSnapshot durante il rendering e dopo ogni notifica, ed esegue un nuovo render solo se il valore è cambiato secondo Object.is.

Perché getSnapshot deve restituire un valore in cache?

React confronta il risultato di ogni chiamata a getSnapshot con quello precedente. Se restituisce ogni volta un nuovo oggetto o array, React vede sempre un cambiamento, esegue un nuovo render, chiama di nuovo getSnapshot, ed entra in un ciclo finché non genera "Maximum update depth exceeded". Restituisci lo stesso riferimento finché i dati non cambiano davvero.

Cos'è getServerSnapshot?

Il terzo argomento facoltativo. Restituisce il valore da usare durante il rendering lato server e durante l'hydration nel browser, così entrambi producono lo stesso HTML. Senza, il componente genera "Missing getServerSnapshot" sul server, e il contenuto sotto il confine <Suspense> più vicino viene invece renderizzato nel browser.

Devo usare useSyncExternalStore o useEffect con useState?

Per iscriverti a dati esterni, preferisci useSyncExternalStore. Legge il valore durante il rendering, quindi il primo rendering è già corretto e ogni componente vede lo stesso valore anche durante il rendering concorrente. useEffect con useState renderizza una volta con un valore vecchio e può mostrare per un attimo valori diversi in componenti diversi.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA