useDeferredValue
useDeferredValue è un Hook React che ti permette di differire l’aggiornamento di una parte dell’UI.
const deferredValue = useDeferredValue(value)Reference
useDeferredValue(value, initialValue?)
Chiama useDeferredValue al top level del tuo componente per ottenere una versione differita di quel valore.
import { useState, useDeferredValue } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}Parameters
value: Il valore che vuoi differire. Può essere di qualsiasi tipo.- optional
initialValue: Un valore da usare durante la renderizzazione iniziale del componente. Se ometti questa opzione,useDeferredValuenon differirà durante la renderizzazione iniziale, perché non esiste una versione precedente divalueda renderizzare al suo posto.
Returns
currentValue: Durante la renderizzazione iniziale, il valore differito restituito saràinitialValue, oppure lo stesso del valore che hai fornito. Durante gli aggiornamenti, React proverà prima a ri-renderizzare con il vecchio valore (quindi restituirà il vecchio valore), e poi tenterà un’altra ri-renderizzazione in background con il nuovo valore (quindi restituirà il valore aggiornato).
Caveats
-
Quando un aggiornamento avviene all’interno di una Transizione,
useDeferredValuerestituisce sempre il nuovovaluee non avvia una renderizzazione differita, perché l’aggiornamento è già differito. -
I valori che passi a
useDeferredValuedovrebbero essere valori primitivi (come stringhe e numeri) oppure oggetti creati al di fuori della renderizzazione. Se crei un nuovo oggetto durante la renderizzazione e lo passi subito auseDeferredValue, sarà diverso a ogni renderizzazione, causando ri-renderizzazioni in background non necessarie. -
Quando
useDeferredValuericeve un valore diverso (rispetto aObject.is), oltre alla renderizzazione corrente (quando usa ancora il valore precedente), pianifica una ri-renderizzazione in background con il nuovo valore. La ri-renderizzazione in background è interrompibile: se c’è un altro aggiornamento avalue, React riavvierà la ri-renderizzazione in background da zero. Per esempio, se l’utente digita in un input più velocemente di quanto un grafico che riceve il suo valore differito riesca a ri-renderizzarsi, il grafico si ri-renderizzerà solo dopo che l’utente smette di digitare. -
useDeferredValueè integrato con<Suspense>. Se l’aggiornamento in background causato da un nuovo valore sospende l’UI, l’utente non vedrà il fallback. Vedrà il vecchio valore differito finché i dati non saranno caricati. -
useDeferredValueda solo non impedisce richieste di rete extra. -
Non c’è un ritardo fisso causato da
useDeferredValuein sé. Non appena React termina la ri-renderizzazione originale, inizierà subito a lavorare sulla ri-renderizzazione in background con il nuovo valore differito. Qualsiasi aggiornamento causato da eventi (come la digitazione) interromperà la ri-renderizzazione in background e avrà priorità su di essa. -
La ri-renderizzazione in background causata da
useDeferredValuenon esegue gli Effetti finché non viene committata sullo schermo. Se la ri-renderizzazione in background sospende, i suoi Effetti verranno eseguiti dopo che i dati saranno caricati e l’UI sarà aggiornata.
Usage
Mostrare contenuto obsoleto mentre il contenuto aggiornato è in caricamento
Chiama useDeferredValue al top level del tuo componente per differire l’aggiornamento di una parte dell’UI.
import { useState, useDeferredValue } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}Durante la renderizzazione iniziale, il valore differito sarà lo stesso del valore che hai fornito.
Durante gli aggiornamenti, il valore differito resterà “indietro” rispetto all’ultimo valore. In particolare, React ri-renderizzerà prima senza aggiornare il valore differito, e poi proverà a ri-renderizzare con il valore appena ricevuto in background.
Vediamo un esempio per capire quando questo è utile.
In questo esempio, il componente SearchResults sospende mentre recupera i risultati della ricerca. Prova a digitare "a", attendi i risultati, e poi modifica in "ab". I risultati per "a" vengono sostituiti dal fallback di caricamento.
import { Suspense, useState } from 'react'; import SearchResults from './SearchResults.js'; export default function App() { const [query, setQuery] = useState(''); return ( <> <label> Search albums: <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Loading...</h2>}> <SearchResults query={query} /> </Suspense> </> ); }
Un pattern UI alternativo comune consiste nel differire l’aggiornamento dell’elenco dei risultati e continuare a mostrare i risultati precedenti finché quelli nuovi non sono pronti. Chiama useDeferredValue per passare in giù una versione differita della query:
export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}query si aggiornerà subito, quindi l’input mostrerà il nuovo valore. Tuttavia, deferredQuery manterrà il valore precedente finché i dati non saranno caricati, quindi SearchResults mostrerà per un po’ i risultati obsoleti.
Digita "a" nell’esempio sotto, attendi che i risultati si carichino, e poi modifica l’input in "ab". Nota come, invece del fallback Suspense, ora vedi l’elenco obsoleto dei risultati finché quelli nuovi non sono caricati:
import { Suspense, useState, useDeferredValue } from 'react'; import SearchResults from './SearchResults.js'; export default function App() { const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query); return ( <> <label> Search albums: <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Loading...</h2>}> <SearchResults query={deferredQuery} /> </Suspense> </> ); }
Approfondimento
Puoi pensarlo come un processo in due passaggi:
-
Per prima cosa, React ri-renderizza con la nuova
query("ab") ma con la vecchiadeferredQuery(ancora"a"). Il valoredeferredQuery, che passi all’elenco dei risultati, è differito: resta “indietro” rispetto al valorequery. -
In background, React prova a ri-renderizzare con entrambi
queryedeferredQueryaggiornati a"ab". Se questa ri-renderizzazione si completa, React la mostrerà sullo schermo. Tuttavia, se sospende (i risultati per"ab"non sono ancora caricati), React abbandonerà questo tentativo di renderizzazione e riproverà questa ri-renderizzazione dopo che i dati saranno caricati. L’utente continuerà a vedere il valore differito obsoleto finché i dati non saranno pronti.
La renderizzazione “in background” differita è interrompibile. Per esempio, se digiti di nuovo nell’input, React la abbandonerà e ripartirà con il nuovo valore. React userà sempre l’ultimo valore fornito.
Nota che c’è comunque una richiesta di rete per ogni pressione di tasto. Ciò che viene differito qui è la visualizzazione dei risultati (finché non sono pronti), non le richieste di rete in sé. Anche se l’utente continua a digitare, le risposte per ogni pressione di tasto vengono messe in cache, quindi premere Backspace è istantaneo e non effettua di nuovo il fetch.
Indicare che il contenuto è obsoleto
Nell’esempio sopra, non c’è alcuna indicazione che l’elenco dei risultati per l’ultima query sia ancora in caricamento. Questo può confondere l’utente se i nuovi risultati impiegano un po’ a caricarsi. Per evidenziare che l’elenco dei risultati non corrisponde all’ultima query, puoi aggiungere un’indicazione visiva quando viene mostrato l’elenco obsoleto:
<div style={{
opacity: query !== deferredQuery ? 0.5 : 1,
}}>
<SearchResults query={deferredQuery} />
</div>Con questa modifica, non appena inizi a digitare, l’elenco obsoleto dei risultati viene leggermente attenuato finché il nuovo elenco non si carica. Puoi anche aggiungere una transizione CSS per ritardare l’attenuazione in modo che risulti graduale, come nell’esempio sotto:
import { Suspense, useState, useDeferredValue } from 'react'; import SearchResults from './SearchResults.js'; export default function App() { const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query); const isStale = query !== deferredQuery; return ( <> <label> Search albums: <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Loading...</h2>}> <div style={{ opacity: isStale ? 0.5 : 1, transition: isStale ? 'opacity 0.2s 0.2s linear' : 'opacity 0s 0s linear' }}> <SearchResults query={deferredQuery} /> </div> </Suspense> </> ); }
Differire la ri-renderizzazione di una parte dell’UI
Puoi anche usare useDeferredValue come ottimizzazione delle prestazioni. È utile quando una parte dell’UI è lenta da ri-renderizzare, non c’è un modo semplice per ottimizzarla, e vuoi evitare che blocchi il resto dell’UI.
Immagina di avere un campo di testo e un componente (come un grafico o un elenco lungo) che si ri-renderizza a ogni pressione di tasto:
function App() {
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={text} />
</>
);
}Per prima cosa, ottimizza SlowList per saltare la ri-renderizzazione quando le sue props sono le stesse. Per farlo, avvolgilo in memo:
const SlowList = memo(function SlowList({ text }) {
// ...
});Tuttavia, questo aiuta solo se le props di SlowList sono le stesse rispetto alla renderizzazione precedente. Il problema che affronti ora è che è lento quando sono diverse, e quando devi effettivamente mostrare un output visivo diverso.
In concreto, il problema principale di prestazioni è che ogni volta che digiti nell’input, SlowList riceve nuove props, e ri-renderizzare l’intero albero rende la digitazione a scatti. In questo caso, useDeferredValue ti permette di dare priorità all’aggiornamento dell’input (che deve essere veloce) rispetto all’aggiornamento dell’elenco dei risultati (che può essere più lento):
function App() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={deferredText} />
</>
);
}Questo non rende più veloce la ri-renderizzazione di SlowList. Tuttavia, dice a React che la ri-renderizzazione dell’elenco può essere deprioritizzata in modo da non bloccare le pressioni di tasto. L’elenco resterà “indietro” rispetto all’input e poi “recupererà”. Come prima, React proverà ad aggiornare l’elenco il prima possibile, ma non bloccherà l’utente dalla digitazione.
Esempio 1 di 2: Ri-renderizzazione differita dell’elenco
In questo esempio, ogni elemento del componente SlowList è artificialmente rallentato così puoi vedere come useDeferredValue ti permette di mantenere l’input reattivo. Digita nell’input e nota come la digitazione resta fluida mentre l’elenco resta “indietro”.
import { useState, useDeferredValue } from 'react'; import SlowList from './SlowList.js'; export default function App() { const [text, setText] = useState(''); const deferredText = useDeferredValue(text); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <SlowList text={deferredText} /> </> ); }
Approfondimento
Ci sono due tecniche di ottimizzazione comuni che potresti aver usato prima in questo scenario:
- Debouncing significa che aspetteresti che l’utente smetta di digitare (per esempio per un secondo) prima di aggiornare l’elenco.
- Throttling significa che aggiorneresti l’elenco ogni tanto (per esempio al massimo una volta al secondo).
Se queste tecniche sono utili in alcuni casi, useDeferredValue è più adatto a ottimizzare la renderizzazione perché è profondamente integrato con React e si adatta al dispositivo dell’utente.
A differenza di debouncing o throttling, non richiede di scegliere un ritardo fisso. Se il dispositivo dell’utente è veloce (per esempio un laptop potente), la ri-renderizzazione differita avverrebbe quasi subito e non sarebbe percettibile. Se il dispositivo dell’utente è lento, l’elenco resterà “indietro” rispetto all’input in proporzione a quanto è lento il dispositivo.
Inoltre, a differenza di debouncing o throttling, le ri-renderizzazioni differite fatte da useDeferredValue sono interrompibili per impostazione predefinita. Questo significa che se React è nel mezzo della ri-renderizzazione di un elenco grande, ma l’utente preme un altro tasto, React abbandonerà quella ri-renderizzazione, gestirà la pressione di tasto, e poi ricomincerà a renderizzare in background. Al contrario, debouncing e throttling producono comunque un’esperienza a scatti perché sono bloccanti: posticipano solo il momento in cui la renderizzazione blocca la pressione di tasto.
Se il lavoro che stai ottimizzando non avviene durante la renderizzazione, debouncing e throttling restano utili. Per esempio, possono farti effettuare meno richieste di rete. Puoi anche usare queste tecniche insieme.