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.

<Suspense> ti permette di mostrare un fallback finché i suoi figli non hanno finito di caricarsi.

<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>

Reference

<Suspense>

Props

  • children: L’UI effettiva che intendi renderizzare. Se children va in sospensione durante la renderizzazione, il boundary Suspense passerà alla renderizzazione di fallback.
  • fallback: Un’UI alternativa da renderizzare al posto dell’UI effettiva se questa non ha finito di caricarsi. Qualsiasi nodo React valido è accettato, anche se in pratica un fallback è una vista segnaposto leggera, come uno spinner di caricamento o uno skeleton. Suspense passerà automaticamente a fallback quando children va in sospensione, e tornerà a children quando i dati sono pronti. Se fallback va in sospensione durante la renderizzazione, attiverà il boundary Suspense padre più vicino.
  • Experimental only optional defer: Un booleano. Quando è true, React può mostrare prima il fallback e renderizzare o fare streaming di children in seguito, anche quando nulla al loro interno va in sospensione. Usalo per contenuto costoso da renderizzare. Il valore predefinito è false.

Caveats

  • Suspense non rileva quando i dati vengono recuperati dentro un Effetto o un gestore di eventi. Si attiva solo nei casi elencati sotto.
  • React non preserva alcuno state per le renderizzazioni andate in sospensione prima di potersi montare per la prima volta. Quando il componente è stato caricato, React ritenterà di renderizzare l’albero sospeso da capo.
  • Se Suspense stava mostrando contenuto per l’albero, ma poi va di nuovo in sospensione, verrà mostrato di nuovo il fallback a meno che l’aggiornamento che lo ha causato non sia stato provocato da startTransition o useDeferredValue.
  • React rivela il contenuto sospeso al massimo una volta ogni 300 ms, misurati dall’ultima rivelazione. I boundary che diventano pronti entro quella finestra vengono rivelati insieme anziché uno alla volta.
  • Se React deve nascondere il contenuto già visibile perché è andato di nuovo in sospensione, ripulirà gli Effetti layout nell’albero del contenuto. Quando il contenuto è pronto per essere mostrato di nuovo, React rieseguirà gli Effetti layout. Questo garantisce che gli Effetti che misurano il layout del DOM non tentino di farlo mentre il contenuto è nascosto.
  • React include ottimizzazioni interne come Streaming Server Rendering e Selective Hydration integrate con Suspense. Leggi una panoramica architetturale e guarda un talk tecnico per saperne di più.

Cosa attiva un boundary Suspense

Un boundary Suspense attende che il suo contenuto sia pronto prima di rivelarlo. Qualsiasi elemento tra i seguenti impedisce a un boundary di rivelare il suo contenuto:

  • Lazy-loading del codice del componente con lazy.
  • Lettura di una Promise con use, inclusi dati in streaming da componenti Server o caricati tramite un framework abilitato a Suspense.
  • Caricamento di un foglio di stile renderizzato con <link rel="stylesheet"> e una prop precedence. React blocca il boundary finché il foglio di stile non è caricato, fino a un timeout. Vedi un esempio sotto.
  • Attesa dell’arrivo dell’HTML di un boundary di grandi dimensioni durante la renderizzazione server in streaming. L’invio dell’HTML richiede tempo, quindi un boundary con contenuto sufficiente si attiva anche quando nulla al suo interno va in sospensione. React rivela il contenuto man mano che l’HTML arriva.
  • Caricamento dei font. Suspense non attende i font per impostazione predefinita, ma un aggiornamento con <ViewTransition> attende il caricamento dei nuovi font, fino a un timeout, così il testo non lampeggia con un font di fallback. Vedi un esempio sotto.
  • Caricamento delle immagini. Suspense non attende le immagini per impostazione predefinita, ma durante un aggiornamento con <ViewTransition>, React blocca il boundary finché l’immagine non è caricata, fino a un timeout. Aggiungere un gestore onLoad esclude una specifica immagine da questo comportamento. Vedi un esempio sotto.
  • Experimental only Esecuzione di lavoro di renderizzazione CPU-bound dentro un boundary <Suspense defer>.

Nota bene

Framework abilitati a Suspense

Un framework abilitato a Suspense ti offre un modo per leggere dati nel tuo componente in modo da attivare il boundary Suspense più vicino. Il modo esatto in cui carichi i dati dipende dal tuo framework e troverai i dettagli nella sua documentazione. Internamente, un framework abilitato a Suspense mantiene una cache di Promise e chiama use per andare in sospensione su una Promise.

Senza un framework, puoi leggere una Promise con use direttamente, purché la Promise sia memorizzata in cache in modo che la stessa istanza venga riutilizzata tra le renderizzazioni.


Usage

Mostrare un fallback mentre il contenuto è in caricamento

Puoi avvolgere qualsiasi parte della tua applicazione con un boundary Suspense:

<Suspense fallback={<Loading />}>
<Albums />
</Suspense>

React mostrerà il tuo fallback di caricamento finché tutto il codice e i dati necessari ai figli non sono stati caricati.

Nell’esempio sotto, il componente Albums va in sospensione mentre recupera l’elenco degli album. Finché non è pronto per la renderizzazione, React passa al boundary Suspense più vicino sopra di esso per mostrare il fallback — il tuo componente Loading. Poi, quando i dati sono caricati, React nasconde il fallback Loading e renderizza il componente Albums con i dati.

import { Suspense } from 'react';
import Albums from './Albums.js';

export default function ArtistPage({ artist }) {
  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<Loading />}>
        <Albums artistId={artist.id} />
      </Suspense>
    </>
  );
}

function Loading() {
  return <h2>🌀 Loading...</h2>;
}

Al contrario, il codice che recupera dati al di fuori di use, ad esempio dentro un Effetto, non attiva il boundary:

import { Suspense } from 'react';
import EffectAlbums from './EffectAlbums.js';

export default function ArtistPage({ artist }) {
  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<Loading />}>
        <EffectAlbums artistId={artist.id} />
      </Suspense>
    </>
  );
}

function Loading() {
  return <h2>🌀 Loading...</h2>;
}

Durante la renderizzazione server in streaming, un boundary si attiva anche mentre il suo HTML è ancora in streaming. Con qualsiasi API di renderizzazione server in streaming, React invia la shell con il fallback per prima, poi fa streaming dell’HTML di ogni boundary e sostituisce il suo fallback man mano che il contenuto arriva. Premi “Render the page” per vedere la pagina arrivare in streaming:

import { flushReadableStreamToFrame } from './demo-helpers.js';
import { Suspense, use } from 'react';
import { renderToReadableStream } from 'react-dom/server';

let posts = null;

function Posts() {
  const text = use(posts.promise);
  return <p>{text}</p>;
}

function ProfilePage() {
  return (
    <html>
      <body>
        <h1>Alice</h1>
        <p>Photographer and traveler.</p>
        <Suspense fallback={<p>⌛ Loading posts...</p>}>
          <Posts />
        </Suspense>
      </body>
    </html>
  );
}

async function main(frame) {
  posts = Promise.withResolvers();
  const stream = await renderToReadableStream(<ProfilePage />);

  // I post vengono risolti dopo che la shell è stata trasmessa in streaming, quindi React
  // fa streaming del loro HTML e sostituisce il fallback.
  setTimeout(() => {
    posts.resolve(
      'Just got back from two weeks along the coast. The drive ' +
      'was longer than expected, but every stop was worth it. ' +
      'A full write-up and more photos are coming soon.'
    );
  }, 1500);

  await flushReadableStreamToFrame(stream, frame);
}

document.getElementById('render').addEventListener('click', () => {
  main(document.getElementById('container'));
});


Rivelare il contenuto tutto insieme

Per impostazione predefinita, l’intero albero dentro Suspense viene trattato come un’unica unità. Per esempio, anche se solo uno di questi componenti va in sospensione in attesa di alcuni dati, tutti insieme verranno sostituiti dall’indicatore di caricamento:

<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>

Poi, quando tutti sono pronti per essere mostrati, appariranno tutti insieme in una volta.

Nell’esempio sotto, sia Biography che Albums recuperano alcuni dati. Tuttavia, poiché sono raggruppati sotto un unico boundary Suspense, questi componenti “compaiono” sempre insieme nello stesso momento.

import { Suspense } from 'react';
import Albums from './Albums.js';
import Biography from './Biography.js';
import Panel from './Panel.js';

export default function ArtistPage({ artist }) {
  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<Loading />}>
        <Biography artistId={artist.id} />
        <Panel>
          <Albums artistId={artist.id} />
        </Panel>
      </Suspense>
    </>
  );
}

function Loading() {
  return <h2>🌀 Loading...</h2>;
}

I componenti che caricano dati non devono essere figli diretti del boundary Suspense. Per esempio, puoi spostare Biography e Albums in un nuovo componente Details. Questo non cambia il comportamento. Biography e Albums condividono lo stesso boundary Suspense padre più vicino, quindi la loro rivelazione è coordinata insieme.

<Suspense fallback={<Loading />}>
<Details artistId={artist.id} />
</Suspense>

function Details({ artistId }) {
return (
<>
<Biography artistId={artistId} />
<Panel>
<Albums artistId={artistId} />
</Panel>
</>
);
}

Rivelare contenuto annidato man mano che si carica

Quando un componente va in sospensione, il componente Suspense padre più vicino mostra il fallback. Questo ti permette di annidare più componenti Suspense per creare una sequenza di caricamento. Il fallback di ogni boundary Suspense verrà sostituito man mano che il livello successivo di contenuto diventa disponibile. Per esempio, puoi dare all’elenco degli album un fallback dedicato:

<Suspense fallback={<BigSpinner />}>
<Biography />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums />
</Panel>
</Suspense>
</Suspense>

Con questa modifica, mostrare Biography non deve “attendere” il caricamento di Albums.

La sequenza sarà:

  1. Se Biography non è ancora caricato, BigSpinner viene mostrato al posto dell’intera area di contenuto.
  2. Una volta che Biography ha finito di caricarsi, BigSpinner viene sostituito dal contenuto.
  3. Se Albums non è ancora caricato, AlbumsGlimmer viene mostrato al posto di Albums e del suo Panel padre.
  4. Infine, una volta che Albums ha finito di caricarsi, sostituisce AlbumsGlimmer.
import { Suspense } from 'react';
import Albums from './Albums.js';
import Biography from './Biography.js';
import Panel from './Panel.js';

export default function ArtistPage({ artist }) {
  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<BigSpinner />}>
        <Biography artistId={artist.id} />
        <Suspense fallback={<AlbumsGlimmer />}>
          <Panel>
            <Albums artistId={artist.id} />
          </Panel>
        </Suspense>
      </Suspense>
    </>
  );
}

function BigSpinner() {
  return <h2>🌀 Loading...</h2>;
}

function AlbumsGlimmer() {
  return (
    <div className="glimmer-panel">
      <div className="glimmer-line" />
      <div className="glimmer-line" />
      <div className="glimmer-line" />
    </div>
  );
}

I boundary Suspense ti permettono di coordinare quali parti della tua UI devono sempre “comparire” insieme nello stesso momento e quali devono rivelare progressivamente più contenuto in una sequenza di stati di caricamento. Puoi aggiungere, spostare o eliminare boundary Suspense in qualsiasi punto dell’albero senza influire sul resto del comportamento della tua app.

Non mettere un boundary Suspense attorno a ogni componente. I boundary Suspense non devono essere più granulari della sequenza di caricamento che vuoi far vivere all’utente. Se lavori con un designer, chiedigli dove posizionare gli stati di caricamento — è probabile che li abbia già inclusi nei wireframe di design.


Mostrare contenuto obsoleto mentre il contenuto aggiornato è in caricamento

In questo esempio, il componente SearchResults va in sospensione mentre recupera i risultati di ricerca. Digita "a", attendi i risultati, poi modifica in "ab". I risultati per "a" verranno 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 posticipare l’aggiornamento dell’elenco e continuare a mostrare i risultati precedenti finché quelli nuovi non sono pronti. L’Hook useDeferredValue ti permette di passare una versione posticipata 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à immediatamente, quindi l’input mostrerà il nuovo valore. Tuttavia, deferredQuery manterrà il valore precedente finché i dati non sono caricati, quindi SearchResults mostrerà per un po’ i risultati obsoleti.

Per renderlo più evidente all’utente, puoi aggiungere un’indicazione visiva quando viene mostrato l’elenco di risultati obsoleti:

<div style={{
opacity: query !== deferredQuery ? 0.5 : 1
}}>
<SearchResults query={deferredQuery} />
</div>

Digita "a" nell’esempio sotto, attendi il caricamento dei risultati, poi modifica l’input in "ab". Nota come, invece del fallback Suspense, ora vedi l’elenco di risultati obsoleti attenuato finché i nuovi risultati 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);
  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 }}>
          <SearchResults query={deferredQuery} />
        </div>
      </Suspense>
    </>
  );
}

Nota bene

Sia i valori posticipati che le Transizioni ti permettono di evitare di mostrare il fallback Suspense a favore di indicatori inline. Le Transizioni segnano l’intero aggiornamento come non urgente, quindi vengono tipicamente usate da framework e librerie router per la navigazione. I valori posticipati, d’altra parte, sono soprattutto utili nel codice applicativo quando vuoi segnare una parte della UI come non urgente e lasciarla “indietro” rispetto al resto della UI.


Impedire che il contenuto già rivelato venga nascosto

Quando un componente va in sospensione, il boundary Suspense padre più vicino passa a mostrare il fallback. Questo può portare a un’esperienza utente brusca se stava già mostrando del contenuto. Prova a premere questo pulsante:

import { Suspense, useState } 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('/');

  function navigate(url) {
    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>
      {content}
    </Layout>
  );
}

function BigSpinner() {
  return <h2>🌀 Loading...</h2>;
}

Quando hai premuto il pulsante, il componente Router ha renderizzato ArtistPage invece di IndexPage. Un componente dentro ArtistPage è andato in sospensione, quindi il boundary Suspense più vicino ha iniziato a mostrare il fallback. Il boundary Suspense più vicino era vicino alla root, quindi l’intero layout del sito è stato sostituito da BigSpinner.

Per evitare questo, puoi segnare l’aggiornamento dello state di navigazione come una Transizione con startTransition:

function Router() {
const [page, setPage] = useState('/');

function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...

Questo dice a React che la transizione di state non è urgente ed è meglio continuare a mostrare la pagina precedente invece di nascondere qualsiasi contenuto già rivelato. Ora cliccare il pulsante “attende” il caricamento di Biography:

import { Suspense, startTransition, useState } 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('/');

  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>
      {content}
    </Layout>
  );
}

function BigSpinner() {
  return <h2>🌀 Loading...</h2>;
}

Una Transizione non attende il caricamento di tutto il contenuto. Attende solo abbastanza a lungo per evitare di nascondere contenuto già rivelato. Per esempio, il Layout del sito era già rivelato, quindi sarebbe negativo nasconderlo dietro uno spinner di caricamento. Tuttavia, il boundary Suspense annidato attorno a Albums è nuovo, quindi la Transizione non attende per esso.

Nota bene

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


Indicare che una Transizione è in corso

Nell’esempio sopra, una volta cliccato il pulsante, non c’è alcuna indicazione visiva che una navigazione è in corso. Per aggiungere un indicatore, puoi sostituire startTransition con useTransition, che ti fornisce un valore booleano isPending. Nell’esempio sotto, viene usato per modificare lo stile dell’header del sito mentre una Transizione è in corso:

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


Reimpostare i boundary Suspense durante la navigazione

Durante una Transizione, React evita di nascondere contenuto già rivelato. Tuttavia, quando navighi verso contenuto diverso, come il profilo di un altro utente, vorrai che il boundary mostri il fallback invece del contenuto precedente. Puoi esprimerlo con una key:

<ProfilePage key={queryParams.id} />

Con una key diversa, React tratta i profili come contenuto diverso e reimposta il boundary Suspense durante la navigazione. La key può essere sul boundary stesso o su un componente sopra di esso. I router integrati con Suspense dovrebbero farlo automaticamente.

Nell’esempio sotto, aprire la pagina del profilo carica il primo profilo. Premendo “Bob” si naviga verso un profilo diverso e la key reimposta il boundary, quindi viene mostrato il fallback invece della bio dell’utente precedente. Prova a rimuovere la key: la bio precedente resta visibile mentre quella successiva si carica:

import { Suspense, useState, startTransition } from 'react';
import Bio from './Bio.js';
import { fetchBio } from './data.js';

export default function ProfilePage() {
  const [user, setUser] = useState(() => ({
    id: 'alice',
    bioPromise: fetchBio('alice'),
  }));
  function navigate(id) {
    startTransition(() => {
      setUser({ id, bioPromise: fetchBio(id) });
    });
  }
  return (
    <>
      <button onClick={() => navigate('alice')}>
        Alice
      </button>
      <button onClick={() => navigate('bob')}>
        Bob
      </button>
      <Suspense key={user.id} fallback={<p>⌛ Loading profile...</p>}>
        <Bio bioPromise={user.bioPromise} />
      </Suspense>
    </>
  );
}


Fornire un fallback per errori server e contenuto solo client

Se usi una delle API di renderizzazione server in streaming (o un framework che si basa su di esse), React userà anche i tuoi boundary <Suspense> per gestire errori sul server. Se un componente lancia un errore sul server, React non interromperà la renderizzazione server. Invece, troverà il componente <Suspense> più vicino sopra di esso e includerà il suo fallback (come uno spinner) nell’HTML server generato. L’utente vedrà inizialmente uno spinner.

Sul client, React tenterà di renderizzare di nuovo lo stesso componente. Se va in errore anche sul client, React lancerà l’errore e mostrerà il contenitore di errori più vicino. Tuttavia, se non va in errore sul client, React non mostrerà l’errore all’utente poiché il contenuto è stato infine mostrato con successo.

Puoi usarlo per escludere alcuni componenti dalla renderizzazione sul server. Per farlo, lancia un errore nell’ambiente server e poi avvolgili in un boundary <Suspense> per sostituire il loro HTML con fallback:

<Suspense fallback={<Loading />}>
<Chat />
</Suspense>

function Chat() {
if (typeof window === 'undefined') {
throw Error('Chat should only render on the client.');
}
// ...
}

L’HTML server includerà l’indicatore di caricamento. Verrà sostituito dal componente Chat sul client.


Fornire un fallback per contenuto solo browser

Un boundary Suspense può fornire un fallback per un componente solo browser. Avvolgi il componente in <Suspense> e chiama use(browser()) al suo interno.

Clicca Reload per vedere il fallback di caricamento nell’HTML iniziale. Dopo l’hydration, React mostra la bozza caricata da localStorage.

import { Suspense, use, useState } from 'react';
import { browser } from 'react-dom';

function SavedDraft() {
  use(browser('The draft is stored in localStorage.'));
  const [draft, setDraft] = useState(
    () => localStorage.getItem('draft') ?? ''
  );

  function handleChange(event) {
    const nextDraft = event.target.value;
    setDraft(nextDraft);
    localStorage.setItem('draft', nextDraft);
  }

  return (
    <label>
      Draft:
      <textarea
        value={draft}
        onChange={handleChange}
        rows={4}
        cols={30}
      />
    </label>
  );
}

export default function App() {
  return (
    <>
      <h1>Saved draft</h1>
      <Suspense fallback={<p>Loading draft...</p>}>
        <SavedDraft />
      </Suspense>
    </>
  );
}

Durante la renderizzazione server, React include il fallback del boundary Suspense nell’HTML. Nel browser, React sostituisce il fallback con la bozza salvata.


Attendere il caricamento di un foglio di stile

Un foglio di stile renderizzato con <link rel="stylesheet"> e una prop precedence blocca il boundary Suspense finché il foglio di stile non è caricato, fino a un timeout, così il contenuto non appare senza stile.

Nell’esempio sotto, il componente Card renderizza un foglio di stile con precedence. Premi “Show card”: React mostra il fallback finché il foglio di stile non è caricato, poi rivela la card con gli stili applicati.

A titolo di confronto, il secondo pulsante esegue lo stesso aggiornamento senza React, in un documento separato. Nulla attende il foglio di stile, quindi il testo della card appare prima in un font di fallback e poi cambia:

import { Suspense, useState, startTransition } from 'react';
import { freshStylesheetUrl } from './styles.js';
import VanillaCard from './VanillaCard.js';

function Card({ href }) {
  return (
    <>
      <link rel="stylesheet" href={href} precedence="default" />
      <div className="fancy-card">This card uses a font from the stylesheet.</div>
    </>
  );
}

export default function App() {
  const [href, setHref] = useState(null);
  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            setHref(freshStylesheetUrl());
          });
        }}>
        Show card
      </button>
      {href && (
        <Suspense fallback={<p>⌛ Loading styles...</p>}>
          <Card href={href} />
        </Suspense>
      )}
      <hr />
      <VanillaCard />
    </>
  );
}


Animare a partire dal contenuto Suspense

Suspense si compone con <ViewTransition> per animare il passaggio dal fallback al contenuto. Avvolgi il boundary in un <ViewTransition> e React tratta lo scambio come un aggiornamento, facendo un cross-fade tra fallback e contenuto per impostazione predefinita:

import {ViewTransition, useState, startTransition, Suspense} from 'react';
import {Video, VideoPlaceholder} from './Video';
import {useLazyVideoData} from './data';

function LazyVideo() {
  const video = useLazyVideoData();
  return <Video video={video} />;
}

export default function Component() {
  const [showItem, setShowItem] = useState(false);
  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            setShowItem((prev) => !prev);
          });
        }}>
        {showItem ? '➖' : '➕'}
      </button>
      {showItem ? (
        <ViewTransition>
          <Suspense fallback={<VideoPlaceholder />}>
            <LazyVideo />
          </Suspense>
        </ViewTransition>
      ) : null}
    </>
  );
}

Nota bene

Dove posizioni il <ViewTransition> rispetto al boundary determina se fallback e contenuto fanno cross-fade come un unico aggiornamento o si animano come animazioni di uscita e ingresso separate. Puoi anche personalizzare l’animazione con le classi View Transition.

Scopri di più sull’animazione a partire dal contenuto Suspense.


Attendere il caricamento di un font

Quando un <ViewTransition> anima la rivelazione di un boundary Suspense, React attende i nuovi font introdotti dal contenuto, fino a un timeout, così il testo non lampeggia con un font di fallback. Questo avviene solo durante un aggiornamento con <ViewTransition>.

Nell’esempio sotto, il boundary Suspense è avvolto in un <ViewTransition> e il componente Quote va in sospensione mentre i suoi dati si caricano. Renderizzare la citazione avvia il download del font. React mantiene visibile il fallback finché il font non è caricato, così la citazione appare già nel suo font.

A titolo di confronto, il secondo pulsante esegue lo stesso aggiornamento senza React. Nulla attende il font, quindi il testo appare prima in un font di fallback e poi cambia:

import { ViewTransition, Suspense, use, useState, startTransition } from 'react';
import { fetchQuote } from './data.js';
import { freshFontUrl } from './font.js';
import VanillaQuote from './VanillaQuote.js';

function Quote({ fontSrc }) {
  const quote = use(fetchQuote());
  return (
    <>
      <style href={fontSrc} precedence="default">
        {`@font-face {
          font-family: 'Fancy';
          src: url(${fontSrc}) format('truetype');
          font-display: swap;
        }`}
      </style>
      <p className="quote fancy">{quote}</p>
    </>
  );
}

export default function App() {
  const [fontSrc, setFontSrc] = useState(null);
  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            setFontSrc(freshFontUrl());
          });
        }}>
        Show quote
      </button>
      {fontSrc && (
        <ViewTransition>
          <Suspense fallback={<p className="quote">⌛ Loading quote...</p>}>
            <Quote fontSrc={fontSrc} />
          </Suspense>
        </ViewTransition>
      )}
      <hr />
      <VanillaQuote />
    </>
  );
}


Attendere il caricamento di un’immagine

Quando un <ViewTransition> anima la rivelazione di un boundary Suspense, React attende il caricamento delle immagini visibili, fino a un timeout, così l’animazione non inizia con un’immagine a metà caricamento. Questo avviene solo durante un aggiornamento con <ViewTransition>. Aggiungere un gestore onLoad esclude una specifica immagine da questo comportamento, anche dentro un <ViewTransition>.

Nell’esempio sotto, il boundary Suspense è avvolto in un <ViewTransition> e mostra uno skeleton del profilo finché il ritratto non è caricato.

A titolo di confronto, il secondo pulsante esegue lo stesso aggiornamento senza React. Nulla attende l’immagine, quindi la card appare immediatamente e l’immagine compare quando si carica:

import { ViewTransition, Suspense, useState, startTransition } from 'react';
import { freshImageUrl } from './image.js';
import VanillaProfile from './VanillaProfile.js';

function Profile({ src }) {
  return (
    <div className="card">
      <img src={src} alt="Jack Pope" width={80} height={80} />
      <p>Jack Pope</p>
    </div>
  );
}

function ProfilePlaceholder() {
  return (
    <div className="card">
      <div className="avatar-placeholder" />
      <p className="name-placeholder">&nbsp;</p>
    </div>
  );
}

export default function App() {
  const [src, setSrc] = useState(null);
  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            setSrc(freshImageUrl());
          });
        }}>
        Show profile
      </button>
      {src && (
        <ViewTransition>
          <Suspense fallback={<ProfilePlaceholder />}>
            <Profile src={src} />
          </Suspense>
        </ViewTransition>
      )}
      <hr />
      <VanillaProfile />
    </>
  );
}


Coordinare font, immagini e fogli di stile

Un boundary Suspense può attendere dati, fogli di stile, font e immagini contemporaneamente. L’attesa di font e immagini avviene solo durante un aggiornamento con <ViewTransition>. Nell’esempio sotto, il componente ProfileCard va in sospensione mentre i suoi dati si caricano e renderizza un foglio di stile con precedence, testo in un nuovo font e un ritratto. React mantiene visibile lo skeleton mentre si caricano i dati e il foglio di stile. La rivelazione con <ViewTransition> attende poi il font e l’immagine, così la card appare completa.

A titolo di confronto, la versione senza React carica gli stessi dati e mostra ogni risorsa arrivare secondo il proprio timing:

import { ViewTransition, Suspense, use, useState, startTransition } from 'react';
import { fetchQuote } from './data.js';
import { freshStylesheetUrl, freshImageUrl } from './resources.js';
import VanillaProfileCard from './VanillaProfileCard.js';

function ProfileCard({ resources }) {
  const quote = use(resources.quotePromise);
  return (
    <>
      <link rel="stylesheet" href={resources.stylesheet} precedence="default" />
      <div className="profile-card">
        <img src={resources.image} alt="Jack Pope" width={80} height={80} />
        <div>
          <p className="name">Jack Pope</p>
          <p className="bio">{quote}</p>
        </div>
      </div>
    </>
  );
}

function ProfileCardPlaceholder() {
  return (
    <div className="profile-card">
      <div className="avatar-placeholder" />
      <div>
        <p className="name name-placeholder">&nbsp;</p>
        <p className="bio bio-placeholder">&nbsp;</p>
      </div>
    </div>
  );
}

export default function App() {
  const [resources, setResources] = useState(null);
  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            setResources({
              quotePromise: fetchQuote(),
              stylesheet: freshStylesheetUrl(),
              image: freshImageUrl(),
            });
          });
        }}>
        Show profile
      </button>
      {resources && (
        <ViewTransition>
          <Suspense fallback={<ProfileCardPlaceholder />}>
            <ProfileCard resources={resources} />
          </Suspense>
        </ViewTransition>
      )}
      <hr />
      <VanillaProfileCard />
    </>
  );
}


Troubleshooting

Come impedisco che la UI venga sostituita da un fallback durante un aggiornamento?

Sostituire una UI visibile con un fallback crea un’esperienza utente brusca. Questo può accadere quando un aggiornamento fa andare in sospensione un componente e il boundary Suspense più vicino sta già mostrando contenuto all’utente.

Per evitare che accada, segna l’aggiornamento come non urgente usando startTransition. Durante una Transizione, React attenderà finché abbastanza dati sono caricati per evitare che appaia un fallback indesiderato:

function handleNextPageClick() {
// Se questo aggiornamento va in sospensione, non nascondere il contenuto già mostrato
startTransition(() => {
setCurrentPage(currentPage + 1);
});
}

Questo eviterà di nascondere il contenuto esistente. Tuttavia, qualsiasi boundary Suspense appena renderizzato mostrerà comunque immediatamente i fallback per evitare di bloccare la UI e lasciare che l’utente veda il contenuto man mano che diventa disponibile.

React impedirà i fallback indesiderati solo durante aggiornamenti non urgenti. Non ritarderà una renderizzazione se è il risultato di un aggiornamento urgente. Devi attivare esplicitamente con un’API come startTransition o useDeferredValue.

Se il tuo router è integrato con Suspense, dovrebbe avvolgere i suoi aggiornamenti in startTransition automaticamente.