<Suspense>
<Suspense>
vous permet d’afficher un contenu de secours en attendant que ses composants enfants aient fini de se charger.
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>
- Référence
- Utilisation
- Afficher une UI de secours pendant le chargement du contenu
- Révéler plusieurs contenus d’un coup
- Révéler des contenus imbriqués au fil du chargement
- Afficher du contenu périmé pendant que le contenu frais charge
- Empêcher le masquage de contenu déjà révélé
- Indiquer qu’une Transition est en cours
- Réinitialiser les périmètres Suspense à la navigation
- Fournir une UI de secours pour les erreurs serveur et le contenu 100% client
- Dépannage
Référence
<Suspense>
Props
children
: l’interface utilisateur (UI) que vous souhaitez effectivement afficher à terme. Sichildren
suspend pendant son rendu, ce périmètre Suspense basculera le rendu surfallback
.fallback
: une UI alternative à afficher au lieu de l’UI finale si celle-ci n’a pas fini de se charger. Ça peut être n’importe quel nœud React valide, mais en pratique une UI de secours est une vue de remplacement légère, telle qu’un spinner ou un squelette structurel. Suspense basculera automatiquement defallback
verschildren
quand les données seront prêtes. Sifallback
suspend pendant son rendu, ça activera le périmètre Suspense parent le plus proche.
Limitations
- React ne préserve pas l’état pour les rendus suspendus avant d’avoir pu faire un premier montage. Une fois le composant chargé, React retentera un rendu de l’arborescence suspendue à partir de zéro.
- Si Suspense affichait du contenu pour l’arborescence, puis est suspendu à nouveau, le
fallback
sera affiché à nouveau à moins que la mise à jour à l’origine de la suspension ait utiliséstartTransition
ouuseDeferredValue
. - Si React a besoin de cacher le contenu déjà visible parce qu’il suspend à nouveau, il nettoiera les Effets de layout pour l’arborescence du contenu. Lorsque le contenu est de nouveau prêt à être affiché, React recommencera à traiter les Effets de rendu. Ça garantit que les Effets qui mesurent la mise en page du DOM n’essaient pas de le faire pendant que le contenu est masqué.
- React inclut des optimisations sous le capot telles que le rendu serveur streamé ou l’hydratation sélective qui sont compatibles avec Suspense. Lisez un survol architectural et regardez cette présentation technique pour en savoir plus. (Les deux ressources sont en anglais, NdT)
Utilisation
Afficher une UI de secours pendant le chargement du contenu
Vous pouvez enrober n’importe quelle partie de votre application dans un périmètre Suspense :
<Suspense fallback={<Loading />}>
<Albums />
</Suspense>
React affichera le contenu de secours jusqu’à ce que le code et les données nécessaires aux enfants aient fini de se charger.
Dans l’exemple ci-dessous, le composant Albums
suspend pendant qu’il charge la liste des albums. Jusqu’à ce qu’il soit prêt à s’afficher, React bascule sur le plus proche périmètre Suspense parent pour en afficher le contenu de secours : votre composant Loading
. Ensuite, une fois les données chargées, React masquera le contenu de secours Loading
et affichera le composant Albums
avec ses données.
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>🌀 Chargement...</h2>; }
Révéler plusieurs contenus d’un coup
Par défaut, toute l’arborescence à l’intérieur de Suspense est considérée comme une unité indivisible. Par exemple, même si un seul de ses composants suspendait en attendant des données, tous les composants seraient remplacés par un indicateur de chargement :
<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>
Ensuite, une fois que tous les composants sont prêts à être affichés, ils apparaitront tous d’un bloc.
Dans l’exemple ci-dessous, les composants Biography
et Albums
chargent des données. Cependant, puisqu’ils appartiennent à un même périmètre Suspense, ces composants « apparaissent » toujours en même temps, d’un bloc.
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>🌀 Chargement...</h2>; }
Les composants qui chargent des données n’ont pas besoin d’être des enfants directs du périmètre Suspense. Par exemple, vous pouvez déplacer Biography
et Albums
dans un nouveau composant Details
: ça ne changera rien au comportement. Biography
et Albums
partagent le même périmètre Suspense parent le plus proche, de sorte qu’ils seront forcément révélés ensemble.
<Suspense fallback={<Loading />}>
<Details artistId={artist.id} />
</Suspense>
function Details({ artistId }) {
return (
<>
<Biography artistId={artistId} />
<Panel>
<Albums artistId={artistId} />
</Panel>
</>
);
}
Révéler des contenus imbriqués au fil du chargement
Lorsqu’un composant suspend, le plus proche composant Suspense parent affiche le contenu de secours. Ça vous permet d’imbriquer plusieurs composants Suspense pour créer des séquences de chargement. Pour chaque périmètre Suspense, le contenu de secours sera remplacé lorsque le niveau suivant de contenu deviendra disponible. Par exemple, vous pouvez donner son propre contenu de secours à la liste des albums :
<Suspense fallback={<BigSpinner />}>
<Biography />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums />
</Panel>
</Suspense>
</Suspense>
Avec ce changement, l’affichage de Biography
n’a plus besoin « d’attendre » qu’Albums
se charge.
La séquence sera :
- Si
Biography
n’est pas encore chargé,BigSpinner
est affiché à la place de l’intégralité du contenu. - Une fois que
Biography
est chargé,BigSpinner
est remplacé par le contenu. - Si
Albums
n’est pas encore chargé,AlbumsGlimmer
est affiché à la place d’Albums
et de son parentPanel
. - Pour finir, une fois
Albums
chargé, il remplaceAlbumsGlimmer
.
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>🌀 Chargement...</h2>; } function AlbumsGlimmer() { return ( <div className="glimmer-panel"> <div className="glimmer-line" /> <div className="glimmer-line" /> <div className="glimmer-line" /> </div> ); }
Les périmètres Suspense vous permettent de coordonner les parties de votre UI qui devraient toujours « débarquer » ensemble, et celles qui devraient révéler progressivement davantage de contenu selon une séquence d’états de chargement. Vous pouvez ajouter, déplacer ou retirer des périmètres Suspense à n’importe quel endroit de l’arbre sans affecter le comportement du reste de l’appli.
Ne mettez pas un périmètre Suspense autour de chaque composant. Les périmètres Suspense ne devraient pas être plus granulaires que la séquence de chargement que vous souhaitez proposer à l’utilisateur. Si vous travaillez avec des designers, demandez-leur où les états de chargement devraient être proposés — ils ont sans doute déjà prévu ça dans leurs maquettes.
Afficher du contenu périmé pendant que le contenu frais charge
Dans cet exemple, le composant SearchResults
suspend pendant le chargement des résultats de recherche. Tapez "a"
, attendez les résultats, puis modifiez votre saisie pour "ab"
. Les résultats pour "a"
seront alors remplacés par le contenu de secours.
import { Suspense, useState } from 'react'; import SearchResults from './SearchResults.js'; export default function App() { const [query, setQuery] = useState(''); return ( <> <label> Rechercher des albums : <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Chargement...</h2>}> <SearchResults query={query} /> </Suspense> </> ); }
Une approche visuelle alternative courante consisterait à différer la mise à jour de la liste et continuer à afficher les résultats précédents jusqu’à ce que les nouveaux résultats soient disponibles. Le Hook useDeferredValue
vous permet de passer une version différée de la requête aux enfants :
export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Rechercher des albums :
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Chargement...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}
La query
sera mise à jour immédiatement, donc le champ affichera la nouvelle valeur. En revanche, la deferredQuery
conservera l’ancienne valeur jusqu’à ce que les données soient chargées, de sorte que SearchResults
affichera des résultats périmés pendant un instant.
Pour que l’utilisateur comprenne ce qui se passe, vous pouvez ajouter un indicateur visuel lorsque la liste affichée est périmée :
<div style={{
opacity: query !== deferredQuery ? 0.5 : 1
}}>
<SearchResults query={deferredQuery} />
</div>
Tapez "a"
dans l’exemple ci-dessous, attendez les résultats, puis modifiez votre saisie pour "ab"
. Constatez qu’au lieu d’afficher le contenu de secours Suspense, vous voyez désormais une liste de résultats périmés assombrie, jusqu’à ce que les nouveaux résultats soient chargés :
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> Rechercher des albums : <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Chargement...</h2>}> <div style={{ opacity: isStale ? 0.5 : 1 }}> <SearchResults query={deferredQuery} /> </div> </Suspense> </> ); }
Empêcher le masquage de contenu déjà révélé
Lorsqu’un composant suspend, le périmètre Suspense parent le plus proche bascule vers le contenu de secours. Ça peut produire une expérience utilisateur désagréable si du contenu était déjà affiché. Essayez d’appuyer sur ce bouton :
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>🌀 Chargement...</h2>; }
Quand vous avez appuyé sur le bouton, le composant Router
a affiché ArtistPage
au lieu de IndexPage
. Un composant au sein d’ArtistPage
a suspendu, du coup le plus proche périmètre Suspense a basculé sur son contenu de secours. Comme ce périmètre était proche de la racine, la mise en page complète du site a été remplacée par BigSpinner
.
Pour éviter ça, vous pouvez indiquer que la mise à jour de l’état de navigation est une Transition, en utilisant startTransition
:
function Router() {
const [page, setPage] = useState('/');
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...
Ça dit à React que cette Transition d’état n’est pas urgente, et qu’il est préférable de continuer à afficher la page précédente plutôt que de masquer du contenu déjà révélé. À présent cliquer sur le bouton « attend » que Biography
soit chargé :
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>🌀 Chargement...</h2>; }
Une Transition n’attend pas que tout le contenu soit chargé. Elle attend seulement assez longtemps pour éviter d’avoir à masquer du contenu déjà révélé. Par exemple, le Layout
du site était déjà révélé, ce serait donc dommage de le masquer derrière un spinner de chargement. En revanche, le périmètre Suspense
imbriqué autour d’Albums
est nouveau, la Transition ne l’attend donc pas.
Indiquer qu’une Transition est en cours
Dans l’exemple précédent, une fois que vous avez cliqué sur le bouton, aucune indication visuelle ne vous informe qu’une navigation est en cours. Pour ajouter une indication, vous pouvez remplacer startTransition
par useTransition
, qui vous donne une valeur booléenne isPending
. Dans l’exemple qui suit, on l’utilise pour modifier le style de l’en-tête du site pendant qu’une Transition est en cours :
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>🌀 Chargement...</h2>; }
Réinitialiser les périmètres Suspense à la navigation
Pendant une Transition, React évitera de masquer du contenu déjà révélé. Ceci dit, lorsque vous naviguez vers une route aux paramètres différents, vous voudrez peut-être indiquer à React que le contenu est différent. Vous pouvez exprimer ça avec une key
:
<ProfilePage key={queryParams.id} />
Imaginez que vous naviguiez au sein d’une page de profil utilisateur, et que quelque chose suspende. Si cette mise à jour est enrobée dans une Transition, elle ne déclenchera pas de contenu de secours pour le contenu déjà visible. C’est bien le comportement attendu.
En revanche, imaginez maintenant que vous naviguiez entre deux profils utilisateurs différents. Dans ce cas, afficher le contenu de secours aurait du sens. Par exemple, le fil des publications d’un utilisateur constitue un contenu différent de celui d’un autre utilisateur. En spécifiant une key
, vous garantissez que React traitera les fils de publications d’utilisateurs différents comme des composants différents, et réinitialisera les périmètres Suspense lors de la navigation. Les routeurs compatibles Suspense sont censés le faire automatiquement.
Fournir une UI de secours pour les erreurs serveur et le contenu 100% client
Si vous utilisez une des API de rendu serveur streamé (ou un framework qui repose dessus), React capitalisera sur vos périmètres <Suspense>
pour le traitement des erreurs survenant côté serveur. Si un composant lève une erreur côté serveur, React n’abandonnera pas le rendu serveur. Il cherchera plutôt le composant parent <Suspense>
le plus proche et incluera son contenu de secours (tel qu’un spinner) dans le HTML généré par le serveur. L’utilisateur verra le spinner pour commencer.
Côté client, React tentera de refaire le rendu de ce composant. Si le client rencontre également des erreurs, React lèvera une erreur et affichera le périmètre d’erreur le plus proche. En revanche, si le rendu côté client fonctionne, React n’affichera aucune erreur à l’utilisateur, puisqu’au final le contenu aura bien pu être affiché.
Vous pouvez tirer parti de ça pour exclure certains composants du rendu serveur. Il vous suffit de lever une erreur lorsque vous faites le rendu côté serveur, et de les enrober dans un périmètre <Suspense>
pour remplacer leur HTML par un contenu de secours :
<Suspense fallback={<Loading />}>
<Chat />
</Suspense>
function Chat() {
if (typeof window === 'undefined') {
throw Error('La discussion ne devrait faire son rendu que côté client.');
}
// ...
}
Le HTML produit par le serveur incluera l’indicateur de chargement. Il sera ensuite remplacé par le composant Chat
coté client.
Dépannage
Comment puis-je empêcher l’UI d’être remplacée par le contenu de secours lors d’une mise à jour ?
Le remplacement d’une UI visible par un contenu de secours produit une expérience utilisateur désagréable. Ça peut arriver lorsqu’une mise à jour entraîne la suspension d’un composant, et que le périmètre Suspense le plus proche affiche déjà du contenu à l’utilisateur.
Pour empêcher ça, indiquez que la mise à jour est non urgente grâce à startTransition
. Pendant la Transition, React attendra jusqu’à ce qu’assez de données aient été chargées, afin d’éviter l’affichage d’un contenu de secours indésirable :
function handleNextPageClick() {
// Si cette mise à jour suspend, ne masque pas du contenu déjà visible
startTransition(() => {
setCurrentPage(currentPage + 1);
});
}
Ça évitera de masquer du contenu déjà visible. En revanche, tout nouveau périmètre Suspense
affichera tout de même immédiatement son contenu de secours pour éviter de bloquer l’UI, et affichera le contenu à l’utilisateur lorsque celui-ci deviendra disponible.
React n’empêchera l’affichage de contenus de secours indésirables que pour les mises à jour non urgentes. Il ne retardera pas le rendu s’il est le résultat d’une mise à jour urgente. Vous devez explicitement utiliser une API telle que startTransition
ou useDeferredValue
.
Remarquez que si votre routeur est intégré avec Suspense, il est censé enrober automatiquement ses mises à jour avec startTransition
.