useTransition

Nota bene

Questa pagina è stata tradotta automaticamente e supervisionata da un maintainer. Un’ulteriore revisione da parte della community sarebbe comunque utile. Migliora questa traduzione.

useTransition è un Hook React che ti permette di renderizzare una parte dell’UI in background.

const [isPending, startTransition] = useTransition()

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();
// ...
}

Vedi altri esempi sotto.

Parameters

useTransition non accetta parametri.

Returns

useTransition restituisce un array con esattamente due elementi:

  1. Il flag isPending che ti indica se c’è una Transizione in sospeso.
  2. La funzione startTransition che 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);
});
}
// ...
}

Nota bene

Le funzioni chiamate in startTransition si chiamano “Action”.

La funzione passata a startTransition si chiama “Action”. Per convenzione, qualsiasi callback chiamata all’interno di startTransition (come una callback prop) dovrebbe chiamarsi action o includere il suffisso “Action”:

function SubmitButton({ submitAction }) {
const [isPending, startTransition] = useTransition();

return (
<button
disabled={isPending}
onClick={() => {
startTransition(async () => {
await submitAction();
});
}}
>
Submit
</button>
);
}

Parameters

  • action: Una funzione che aggiorna lo state chiamando una o più funzioni set. React chiama action immediatamente senza parametri e contrassegna come Transizioni tutti gli aggiornamenti di state pianificati in modo sincrono durante la chiamata alla funzione action. Qualsiasi chiamata async attesa con await in action sarà inclusa nella Transizione, ma attualmente richiede di avvolgere qualsiasi funzione set dopo await in un ulteriore startTransition (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 standalone startTransition.

  • Puoi avvolgere un aggiornamento in una Transizione solo se hai accesso alla funzione set di quello state. Se vuoi avviare una Transizione in risposta a una prop o a un valore di un custom Hook, prova useDeferredValue.

  • La funzione che passi a startTransition viene chiamata immediatamente, contrassegnando come Transizioni tutti gli aggiornamenti di state che avvengono mentre viene eseguita. Se provi a eseguire aggiornamenti di state in un setTimeout, per esempio, non saranno contrassegnati come Transizioni.

  • Devi avvolgere qualsiasi aggiornamento di state dopo richieste async in un altro startTransition per contrassegnarli come Transizioni. Questa è una limitazione nota che correggeremo in futuro (vedi Troubleshooting).

  • La funzione startTransition ha 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:

  1. Il flag isPending che ti indica se c’è una Transizione in sospeso.
  2. La funzione startTransition che 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.

La differenza tra Action e gestione ordinaria degli eventi

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>
  );
}

Nota bene

Quando esponi una prop action da un componente, dovresti fare await al suo interno nella Transizione.

Questo permette alla callback action di essere sincrona o asincrona senza richiedere un ulteriore startTransition per avvolgere await nell’action.


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.

Nota bene

Le Transizioni “attendono” solo abbastanza a lungo per evitare di nascondere contenuto già rivelato (come il contenitore delle tab). Se la tab Posts avesse un boundary <Suspense> annidato, la Transizione non “attenderebbe” per esso.


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:

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>;
}

Nota bene

I router abilitati a Suspense dovrebbero avvolgere gli aggiornamenti di navigazione in Transizioni per impostazione predefinita.


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:

  1. 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.
  2. In alternativa, puoi avere una variabile di state e aggiungere useDeferredValue che 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>
  );
}