useTransition
useTransition è un Hook React che ti permette di renderizzare una parte dell’UI in background.
const [isPending, startTransition] = useTransition()- Reference
- Usage
- Troubleshooting
- Aggiornare un input in una Transizione non funziona
- React non tratta il mio aggiornamento di state come Transizione
- React non tratta il mio aggiornamento di state dopo
awaitcome Transizione - Voglio chiamare
useTransitiondall’esterno di un componente - La funzione che passo a
startTransitionviene eseguita immediatamente - I miei aggiornamenti di state nelle Transizioni sono fuori ordine
Reference
useTransition()
Chiama useTransition al top level del tuo componente per contrassegnare alcuni aggiornamenti di state come Transizioni.
import { useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}Parameters
useTransition non accetta parametri.
Returns
useTransition restituisce un array con esattamente due elementi:
- Il flag
isPendingche ti indica se c’è una Transizione in sospeso. - La funzione
startTransitionche ti permette di contrassegnare gli aggiornamenti come Transizione.
startTransition(action)
La funzione startTransition restituita da useTransition ti permette di contrassegnare un aggiornamento come Transizione.
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}Parameters
action: Una funzione che aggiorna lo state chiamando una o più funzioniset. React chiamaactionimmediatamente senza parametri e contrassegna come Transizioni tutti gli aggiornamenti di state pianificati in modo sincrono durante la chiamata alla funzioneaction. Qualsiasi chiamata async attesa conawaitinactionsarà inclusa nella Transizione, ma attualmente richiede di avvolgere qualsiasi funzionesetdopoawaitin un ulteriorestartTransition(vedi Troubleshooting). Gli aggiornamenti di state contrassegnati come Transizioni saranno non bloccanti e non mostreranno indicatori di caricamento indesiderati.
Returns
startTransition non restituisce nulla.
Caveats
-
useTransitionè un Hook, quindi può essere chiamato solo all’interno di componenti o custom Hook. Se devi avviare una Transizione altrove (per esempio, da una libreria di dati), chiama invece la funzione standalonestartTransition. -
Puoi avvolgere un aggiornamento in una Transizione solo se hai accesso alla funzione
setdi quello state. Se vuoi avviare una Transizione in risposta a una prop o a un valore di un custom Hook, provauseDeferredValue. -
La funzione che passi a
startTransitionviene chiamata immediatamente, contrassegnando come Transizioni tutti gli aggiornamenti di state che avvengono mentre viene eseguita. Se provi a eseguire aggiornamenti di state in unsetTimeout, per esempio, non saranno contrassegnati come Transizioni. -
Devi avvolgere qualsiasi aggiornamento di state dopo richieste async in un altro
startTransitionper contrassegnarli come Transizioni. Questa è una limitazione nota che correggeremo in futuro (vedi Troubleshooting). -
La funzione
startTransitionha un’identità stabile, quindi spesso la vedrai omessa dalle dipendenze degli Effetti, ma includerla non farà scattare l’Effetto. Se il linter ti permette di omettere una dipendenza senza errori, puoi farlo in sicurezza. Scopri di più sulla rimozione delle dipendenze degli Effetti. -
Un aggiornamento di state contrassegnato come Transizione verrà interrotto da altri aggiornamenti di state. Per esempio, se aggiorni un componente grafico all’interno di una Transizione, ma poi inizi a digitare in un input mentre il grafico è nel mezzo di una ri-renderizzazione, React riavvierà il lavoro di renderizzazione sul componente grafico dopo aver gestito l’aggiornamento dell’input.
-
Gli aggiornamenti Transizione non possono essere usati per controllare input di testo.
-
Se ci sono più Transizioni in corso, React attualmente le raggruppa insieme. Questa è una limitazione che potrebbe essere rimossa in una release futura.
Usage
Eseguire aggiornamenti non bloccanti con le Action
Chiama useTransition in cima al tuo componente per creare Action e accedere allo state pending:
import {useState, useTransition} from 'react';
function CheckoutForm() {
const [isPending, startTransition] = useTransition();
// ...
}useTransition restituisce un array con esattamente due elementi:
- Il flag
isPendingche ti indica se c’è una Transizione in sospeso. - La funzione
startTransitionche ti permette di creare un’Action.
Per avviare una Transizione, passa una funzione a startTransition così:
import {useState, useTransition} from 'react';
import {updateQuantity} from './api';
function CheckoutForm() {
const [isPending, startTransition] = useTransition();
const [quantity, setQuantity] = useState(1);
function onSubmit(newQuantity) {
startTransition(async function () {
const savedQuantity = await updateQuantity(newQuantity);
startTransition(() => {
setQuantity(savedQuantity);
});
});
}
// ...
}La funzione passata a startTransition si chiama “Action”. Puoi aggiornare lo state e (opzionalmente) eseguire effetti collaterali all’interno di un’Action, e il lavoro verrà svolto in background senza bloccare le interazioni dell’utente sulla pagina. Una Transizione può includere più Action, e mentre una Transizione è in corso, la tua UI resta reattiva. Per esempio, se l’utente clicca una tab ma poi cambia idea e ne clicca un’altra, il secondo click verrà gestito immediatamente senza attendere il completamento del primo aggiornamento.
Per dare all’utente un feedback sulle Transizioni in corso, lo state isPending passa a true alla prima chiamata a startTransition e resta true finché tutte le Action non sono completate e lo state finale viene mostrato all’utente. Le Transizioni assicurano che gli effetti collaterali nelle Action si completino in ordine per prevenire indicatori di caricamento indesiderati, e puoi fornire un feedback immediato mentre la Transizione è in corso con useOptimistic.
Esempio 1 di 2: Aggiornare la quantità in un’Action
In questo esempio, la funzione updateQuantity simula una richiesta al server per aggiornare la quantità dell’articolo nel carrello. Questa funzione è artificialmente rallentata in modo che impieghi almeno un secondo per completare la richiesta.
Aggiorna la quantità più volte in rapida successione. Nota che lo state pending “Total” viene mostrato mentre le richieste sono in corso, e “Total” si aggiorna solo dopo che l’ultima richiesta è completata. Poiché l’aggiornamento è in un’Action, la “quantity” può continuare ad essere aggiornata mentre la richiesta è in corso.
import { useState, useTransition } from "react"; import { updateQuantity } from "./api"; import Item from "./Item"; import Total from "./Total"; export default function App({}) { const [quantity, setQuantity] = useState(1); const [isPending, startTransition] = useTransition(); const updateQuantityAction = async newQuantity => { // Per accedere allo state pending di una Transizione, // chiama di nuovo startTransition. startTransition(async () => { const savedQuantity = await updateQuantity(newQuantity); startTransition(() => { setQuantity(savedQuantity); }); }); }; return ( <div> <h1>Checkout</h1> <Item action={updateQuantityAction}/> <hr /> <Total quantity={quantity} isPending={isPending} /> </div> ); }
Questo è un esempio base per dimostrare come funzionano le Action, ma non gestisce richieste che si completano fuori ordine. Quando aggiorni la quantità più volte, è possibile che le richieste precedenti finiscano dopo quelle successive, causando un aggiornamento della quantità fuori ordine. Questa è una limitazione nota che correggeremo in futuro (vedi Troubleshooting sotto).
Per casi d’uso comuni, React fornisce astrazioni integrate come:
Queste soluzioni gestiscono l’ordinamento delle richieste per te. Quando usi le Transizioni per costruire i tuoi custom Hook o librerie che gestiscono transizioni async di state, hai un controllo maggiore sull’ordinamento delle richieste, ma devi gestirlo tu stesso.
Esporre la prop action dai componenti
Puoi esporre una prop action da un componente per permettere a un genitore di chiamare un’Action.
Per esempio, questo componente TabButton avvolge la sua logica onClick in una prop action:
export default function TabButton({ action, children, isActive }) {
const [isPending, startTransition] = useTransition();
if (isActive) {
return <b>{children}</b>
}
return (
<button onClick={() => {
startTransition(async () => {
// await all'action passata.
// Questo permette che sia sincrona o asincrona.
await action();
});
}}>
{children}
</button>
);
}Poiché il componente genitore aggiorna il suo state all’interno di action, quell’aggiornamento di state viene contrassegnato come Transizione. Questo significa che puoi cliccare su “Posts” e poi cliccare immediatamente su “Contact” senza bloccare le interazioni dell’utente:
import { useTransition } from 'react'; export default function TabButton({ action, children, isActive }) { const [isPending, startTransition] = useTransition(); if (isActive) { return <b>{children}</b> } if (isPending) { return <b className="pending">{children}</b>; } return ( <button onClick={async () => { startTransition(async () => { // await all'action passata. // Questo permette che sia sincrona o asincrona. await action(); }); }}> {children} </button> ); }
Mostrare uno state visivo pending
Puoi usare il valore booleano isPending restituito da useTransition per indicare all’utente che una Transizione è in corso. Per esempio, il pulsante tab può avere uno state visivo speciale “pending”:
function TabButton({ action, children, isActive }) {
const [isPending, startTransition] = useTransition();
// ...
if (isPending) {
return <b className="pending">{children}</b>;
}
// ...Nota come cliccare su “Posts” ora sembri più reattivo perché il pulsante tab stesso si aggiorna subito:
import { useTransition } from 'react'; export default function TabButton({ action, children, isActive }) { const [isPending, startTransition] = useTransition(); if (isActive) { return <b>{children}</b> } if (isPending) { return <b className="pending">{children}</b>; } return ( <button onClick={() => { startTransition(async () => { await action(); }); }}> {children} </button> ); }
Prevenire indicatori di caricamento indesiderati
In questo esempio, il componente PostsTab recupera dei dati usando use. Quando clicchi la tab “Posts”, il componente PostsTab sospende, causando la comparsa del fallback di caricamento più vicino:
import { Suspense, useState } from 'react'; import TabButton from './TabButton.js'; import AboutTab from './AboutTab.js'; import PostsTab from './PostsTab.js'; import ContactTab from './ContactTab.js'; export default function TabContainer() { const [tab, setTab] = useState('about'); return ( <Suspense fallback={<h1>🌀 Loading...</h1>}> <TabButton isActive={tab === 'about'} action={() => setTab('about')} > About </TabButton> <TabButton isActive={tab === 'posts'} action={() => setTab('posts')} > Posts </TabButton> <TabButton isActive={tab === 'contact'} action={() => setTab('contact')} > Contact </TabButton> <hr /> {tab === 'about' && <AboutTab />} {tab === 'posts' && <PostsTab />} {tab === 'contact' && <ContactTab />} </Suspense> ); }
Nascondere l’intero contenitore delle tab per mostrare un indicatore di caricamento produce un’esperienza utente brusca. Se aggiungi useTransition a TabButton, puoi invece mostrare lo state pending nel pulsante tab.
Nota che cliccare su “Posts” non sostituisce più l’intero contenitore delle tab con uno spinner:
import { useTransition } from 'react'; export default function TabButton({ action, children, isActive }) { const [isPending, startTransition] = useTransition(); if (isActive) { return <b>{children}</b> } if (isPending) { return <b className="pending">{children}</b>; } return ( <button onClick={() => { startTransition(async () => { await action(); }); }}> {children} </button> ); }
Leggi di più sull’uso delle Transizioni con Suspense.
Costruire un router abilitato a Suspense
Se stai costruendo un framework React o un router, ti consigliamo di contrassegnare le navigazioni di pagina come Transizioni.
function Router() {
const [page, setPage] = useState('/');
const [isPending, startTransition] = useTransition();
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...Questo è consigliato per tre motivi:
- Le Transizioni sono interrompibili, il che permette all’utente di cliccare altrove senza attendere il completamento della ri-renderizzazione.
- Le Transizioni prevengono indicatori di caricamento indesiderati, il che permette all’utente di evitare salti bruschi durante la navigazione.
- Le Transizioni attendono che tutte le Action in sospeso si completino, il che permette all’utente di attendere il completamento degli effetti collaterali prima che la nuova pagina venga mostrata.
Ecco un esempio semplificato di router che usa le Transizioni per le navigazioni.
import { Suspense, useState, useTransition } from 'react'; import IndexPage from './IndexPage.js'; import ArtistPage from './ArtistPage.js'; import Layout from './Layout.js'; export default function App() { return ( <Suspense fallback={<BigSpinner />}> <Router /> </Suspense> ); } function Router() { const [page, setPage] = useState('/'); const [isPending, startTransition] = useTransition(); function navigate(url) { startTransition(() => { setPage(url); }); } let content; if (page === '/') { content = ( <IndexPage navigate={navigate} /> ); } else if (page === '/the-beatles') { content = ( <ArtistPage artist={{ id: 'the-beatles', name: 'The Beatles', }} /> ); } return ( <Layout isPending={isPending}> {content} </Layout> ); } function BigSpinner() { return <h2>🌀 Loading...</h2>; }
Mostrare un errore agli utenti con un error boundary
Se una funzione passata a startTransition lancia un errore o restituisce una Promise rifiutata, puoi mostrare un errore al tuo utente con un error boundary. Per usare un error boundary, avvolgi il componente in cui chiami useTransition in un error boundary. Una volta che la funzione passata a startTransition genera un errore, verrà mostrato il fallback dell’error boundary.
import { useTransition } from "react"; import { ErrorBoundary } from "react-error-boundary"; export function AddCommentContainer() { return ( <ErrorBoundary fallback={<p>⚠️Something went wrong</p>}> <AddCommentButton /> </ErrorBoundary> ); } function addComment(comment) { // A scopo dimostrativo per mostrare l'Error Boundary if (comment == null) { throw new Error("Example Error: An error thrown to trigger error boundary"); } } function AddCommentButton() { const [pending, startTransition] = useTransition(); return ( <button disabled={pending} onClick={() => { startTransition(() => { // Intenzionalmente non passa un commento // così viene lanciato un errore addComment(); }); }} > Add comment </button> ); }
Troubleshooting
Aggiornare un input in una Transizione non funziona
Non puoi usare una Transizione per una variabile di state che controlla un input:
const [text, setText] = useState('');
// ...
function handleChange(e) {
// ❌ Non puoi usare Transizioni per lo state di un input controllato
startTransition(() => {
setText(e.target.value);
});
}
// ...
return <input value={text} onChange={handleChange} />;Questo perché le Transizioni sono non bloccanti, ma aggiornare un input in risposta all’evento change dovrebbe avvenire in modo sincrono. Se vuoi eseguire una Transizione in risposta alla digitazione, hai due opzioni:
- Puoi dichiarare due variabili di state separate: una per lo state dell’input (che si aggiorna sempre in modo sincrono) e una che aggiornerai in una Transizione. Questo ti permette di controllare l’input usando lo state sincrono e passare la variabile di state Transizione (che resterà “indietro” rispetto all’input) al resto della tua logica di renderizzazione.
- In alternativa, puoi avere una variabile di state e aggiungere
useDeferredValueche resterà “indietro” rispetto al valore reale. Attiverà ri-renderizzazioni non bloccanti per “recuperare” automaticamente il nuovo valore.
React non tratta il mio aggiornamento di state come Transizione
Quando avvolgi un aggiornamento di state in una Transizione, assicurati che avvenga durante la chiamata a startTransition:
startTransition(() => {
// ✅ Impostare lo state *durante* la chiamata a startTransition
setPage('/about');
});La funzione che passi a startTransition deve essere sincrona. Non puoi contrassegnare un aggiornamento come Transizione così:
startTransition(() => {
// ❌ Impostare lo state *dopo* la chiamata a startTransition
setTimeout(() => {
setPage('/about');
}, 1000);
});Invece, potresti fare così:
setTimeout(() => {
startTransition(() => {
// ✅ Impostare lo state *durante* la chiamata a startTransition
setPage('/about');
});
}, 1000);React non tratta il mio aggiornamento di state dopo await come Transizione
Quando usi await all’interno di una funzione startTransition, gli aggiornamenti di state che avvengono dopo await non sono contrassegnati come Transizioni. Devi avvolgere gli aggiornamenti di state dopo ogni await in una chiamata a startTransition:
startTransition(async () => {
await someAsyncFunction();
// ❌ Non usare startTransition dopo await
setPage('/about');
});Tuttavia, funziona così:
startTransition(async () => {
await someAsyncFunction();
// ✅ Usare startTransition *dopo* await
startTransition(() => {
setPage('/about');
});
});Questa è una limitazione di JavaScript dovuta al fatto che React perde lo scope del contesto async. In futuro, quando AsyncContext sarà disponibile, questa limitazione verrà rimossa.
Voglio chiamare useTransition dall’esterno di un componente
Non puoi chiamare useTransition fuori da un componente perché è un Hook. In questo caso, la funzione standalone startTransition può contrassegnare gli aggiornamenti di state come Transizioni. Non fornisce il flag isPending. Poiché la funzione standalone non è associata a un componente, un Error Boundary non può gestire errori dalla sua Transizione.
La funzione che passo a startTransition viene eseguita immediatamente
Se esegui questo codice, stamperà 1, 2, 3:
console.log(1);
startTransition(() => {
console.log(2);
setPage('/about');
});
console.log(3);È previsto che stampi 1, 2, 3. La funzione che passi a startTransition non viene ritardata. A differenza del setTimeout del browser, non esegue la callback in seguito. React esegue la tua funzione immediatamente, ma qualsiasi aggiornamento di state pianificato mentre è in esecuzione viene contrassegnato come Transizione. Puoi immaginare che funzioni così:
// Una versione semplificata di come funziona React
let isInsideTransition = false;
function startTransition(scope) {
isInsideTransition = true;
scope();
isInsideTransition = false;
}
function setState() {
if (isInsideTransition) {
// ... pianifica un aggiornamento di state Transizione ...
} else {
// ... pianifica un aggiornamento di state urgente ...
}
}I miei aggiornamenti di state nelle Transizioni sono fuori ordine
Se fai await all’interno di startTransition, potresti vedere gli aggiornamenti avvenire fuori ordine.
In questo esempio, la funzione updateQuantity simula una richiesta al server per aggiornare la quantità dell’articolo nel carrello. Questa funzione artificialmente restituisce ogni altra richiesta dopo la precedente per simulare race condition nelle richieste di rete.
Prova ad aggiornare la quantità una volta, poi aggiornarla rapidamente più volte. Potresti vedere il totale errato:
import { useState, useTransition } from "react"; import { updateQuantity } from "./api"; import Item from "./Item"; import Total from "./Total"; export default function App({}) { const [quantity, setQuantity] = useState(1); const [isPending, startTransition] = useTransition(); // Memorizza la quantità effettiva in uno state separato per mostrare la discrepanza. const [clientQuantity, setClientQuantity] = useState(1); const updateQuantityAction = newQuantity => { setClientQuantity(newQuantity); // Accedi allo state pending della Transizione // avvolgendo di nuovo in startTransition. startTransition(async () => { const savedQuantity = await updateQuantity(newQuantity); startTransition(() => { setQuantity(savedQuantity); }); }); }; return ( <div> <h1>Checkout</h1> <Item action={updateQuantityAction}/> <hr /> <Total clientQuantity={clientQuantity} savedQuantity={quantity} isPending={isPending} /> </div> ); }
Quando clicchi più volte, è possibile che le richieste precedenti finiscano dopo quelle successive. Quando succede, React attualmente non ha modo di conoscere l’ordine previsto. Questo perché gli aggiornamenti sono pianificati in modo asincrono e React perde il contesto dell’ordine attraverso il confine async.
Questo è previsto, perché le Action all’interno di una Transizione non garantiscono l’ordine di esecuzione. Per casi d’uso comuni, React fornisce astrazioni di livello superiore come useActionState e action di <form> che gestiscono l’ordinamento per te. Per casi d’uso avanzati, dovrai implementare la tua logica di accodamento e abort per gestirlo.
Esempio di useActionState che gestisce l’ordine di esecuzione:
import { useState, useActionState } from "react"; import { updateQuantity } from "./api"; import Item from "./Item"; import Total from "./Total"; export default function App({}) { // Memorizza la quantità effettiva in uno state separato per mostrare la discrepanza. const [clientQuantity, setClientQuantity] = useState(1); const [quantity, updateQuantityAction, isPending] = useActionState( async (prevState, payload) => { setClientQuantity(payload); const savedQuantity = await updateQuantity(payload); return savedQuantity; // Restituisce la nuova quantità per aggiornare lo state }, 1 // Initial quantity ); return ( <div> <h1>Checkout</h1> <Item action={updateQuantityAction}/> <hr /> <Total clientQuantity={clientQuantity} savedQuantity={quantity} isPending={isPending} /> </div> ); }