<Suspense>
<Suspense>
te permite mostrar una interfaz alternativa o fallback hasta que sus hijos hayan terminado de cargar.
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>
- Referencia
- Uso
- Visualización de una interfaz alternativa mientras se carga el contenido
- Revelar contenido todo de una vez
- Revelar el contenido anidado mientras se carga
- Mostrar contenido antiguo mientras se carga el nuevo
- Prevenir que el contenido ya revelado se esconda
- Indicar que está ocurriendo una transición
- Reiniciar las barreras de Suspense al navegar
- Proporcionar un fallback para los errores del servidor y el contenido exclusivo del servidor
- Solución de problemas
Referencia
<Suspense>
Props
children
: La interfaz que realmente se pretende renderizar. Sichildren
se suspende mientras se renderiza, la barrera de Suspense pasará a renderizarfallback
.fallback
: Una interfaz alternativa a renderizar en lugar de la interfaz real si esta no ha terminado de cargar. Se acepta cualquier nodo React válido, aunque en la práctica, un fallback es una vista ligera de relleno, como un spinner de carga o un esqueleto. Suspense cambiará automáticamente afallback
cuandochildren
se suspenda, y volverá achildren
cuando los datos estén listos. Sifallback
se suspende mientras se renderiza, activará la barrera de Suspense padre más cercana.
Advertencias
- React no preserva ningún estado para los renderizados que se suspendieron antes de que pudieran montarse por primera vez. Cuando el componente se haya cargado, React volverá a intentar renderizar el árbol suspendido desde cero.
- Si la suspensión estaba mostrando contenido para el árbol, pero luego se volvió a suspender, el
fallback
se mostrará de nuevo a menos que la actualización que lo causó fuese causada porstartTransition
ouseDeferredValue
. - Si React necesita ocultar el contenido ya visible porque se suspendió de nuevo, limpiará los Efectos de layout en el árbol de contenido. Cuando el contenido esté listo para mostrarse de nuevo, React disparará los Efectos de layout de nuevo. Esto le permite asegurarse de que los Efectos que miden el diseño del DOM no intentan hacerlo mientras el contenido está oculto.
- React incluye optimizaciones internas como Renderizado en el servidor con Streaming e Hidratación selectiva que se integran con Suspense. Puedes leer una visión general de la arquitectura y ver esta charla técnica para conocer más.
Uso
Visualización de una interfaz alternativa mientras se carga el contenido
Puedes envolver cualquier parte de la aplicación con un barrera de Suspense:
<Suspense fallback={<Loading />}>
<Albums />
</Suspense>
React mostrará tu fallback de carga hasta que se haya cargado todo el código y los datos que necesiten los hijos.
En el ejemplo de abajo, el componente Albums
se suspende mientras carga la lista de álbumes. Hasta que no esté listo para renderizar, React hace que la barrera de Suspense más próxima desde arriba cambie a mostrar el fallback: tu componente Loading
. Luego, una vez que se carguen los datos, React esconde el fallback Loading
y renderiza el componente Albums
con datos.
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>; }
Revelar contenido todo de una vez
Por defecto, todo el árbol dentro de Suspense se trata como una sola unidad. Por ejemplo, incluso si solo uno de estos componentes se suspende mientras espera por algunos datos, todos juntos serán reemplazados por el indicador de carga:
<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>
Luego, una vez que todos estén listos para mostrarse, aparecerán todos de una vez.
En el ejemplo de abajo, tanto Biography
como Albums
cargan algunos datos. Sin embargo, como están agrupados en la misma barrera de Suspense, estos componentes siempre «aparecen* juntos al mismo tiempo.
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>; }
Los componentes que cargan datos no tienen que ser hijos directos de una barrera de Suspense. Por ejemplo, puedes mover Biography
y Albums
dentro de un nuevo componente Details
. Esto no cambia el comportamiento. Como Biography
y Albums
comparten la misma barrera de Suspense más cercana, se muestran juntos de forma coordinada.
<Suspense fallback={<Loading />}>
<Details artistId={artist.id} />
</Suspense>
function Details({ artistId }) {
return (
<>
<Biography artistId={artistId} />
<Panel>
<Albums artistId={artistId} />
</Panel>
</>
);
}
Revelar el contenido anidado mientras se carga
Cuando un componente se suspende, el componente Suspense padre más cercan muestra el fallback. Esto te permite anidar varios componentes Suspense para crear una secuencia de carga. El fallback de cada barrera de Suspense se rellenará a medida que el siguiente nivel de contenido esté disponible. Por ejemplo, puedes darle su propio fallback de carga a la lista de álbumes:
<Suspense fallback={<BigSpinner />}>
<Biography />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums />
</Panel>
</Suspense>
</Suspense>
Con este cambio, no se necesita esperar por que cargue Albums
para mostrar Biography
.
La secuencia sería:
- Si
Biography
aún no ha cargado, se muestraBigSpinner
en lugar de toda el área de contenido. - Una vez que
Biography
termine de cargar,BigSpinner
se reemplaza por el contenido. - Si
Albums
aún no ha cargado, se muestraAlbumsGlimmer
en lugar deAlbums
y su padrePanel
. - Por último, una vez que
Albums
termina de cargar, reemplaza aAlbumsGlimmer
.
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> ); }
Las barrearas de Suspense te permiten coordinar qué partes de tu UI deben siempre «aparecer» juntas al mismo tiempo y qué partes deberían revelar progresivamente más contenido en una secuencia de estados de carga. Puedes añadir, mover o eliminar barreras de Suspense en cualquier lugar del árbol sin afectar el comportamiento restante de tu aplicación.
No pongas una barrera de Suspense alrededor de cada componte. Las barreras de Suspense no deberían ser más granulares que la secuencia de carga que quieres que el usuario experimente. Si trabajas con un diseñador, pregúntale dónde deben colocarse los estados de carga —es probable que ya los hayan incluido en el diseño de sus wireframes.
Mostrar contenido antiguo mientras se carga el nuevo
En este ejemplo, el componente SearchResults
se suspende mientras carga los resultados de búsqueda. Intenta escribir "a"
, espera por los resultados, y luego edítalo a "ab"
. Los resultados para "a"
se reemplazarán por el fallback de carga.
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 patrón de UI común consiste en aplazar la actualización de la lista de resultados y seguir mostrando los resultados anteriores hasta que los nuevos resultados estén listos. El Hook useDeferredValue
te permite pasar una versión aplazada de la consulta:
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>
</>
);
}
La consulta query
se actualizará inmediatamente, por lo que el input mostrará el nuevo valor. Sin embargo, la consulta aplazada deferredQuery
mostrará el valor anterior hasta que los datos se hayan cargado, por lo que SearchResults
mostrará los resultados antiguos por un tiempo.
Para que le resulte más claro al usuario, puedes añadir un indicador visual cuando la lista de resultados antigua se esté mostrando:
<div style={{
opacity: query !== deferredQuery ? 0.5 : 1
}}>
<SearchResults query={deferredQuery} />
</div>
Escribe "a"
en el ejemplo de abajo, espera por los resultados, y luego edita el input a "ab"
. Fíjate cómo en lugar del fallback de Suspense, verás ahora de forma ligeramente atenuada la lista de resultados antigua hasta que se carguen los nuevos resultados:
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> </> ); }
Prevenir que el contenido ya revelado se esconda
Cuando un componente se suspende, la barrera padre de Suspense más cercana cambia a mostrar el fallback. Esto puede conducir a una experiencia de usuario discordante si ya estaba mostrando algún contenido. Presiona el botón en el ejemplo de abajo:
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>; }
Cuando presionaste el botón, el componente Router
renderizó ArtistPage
en lugar de IndexPage
. Un componente dentro de ArtistPage
se suspendió, por lo que la barrera de Suspense más cercana comenzó a mostrar un fallback La barrera de Suspense más cercana estaba cerca de la raíz, por lo que todo el sitio quedó reemplazado por BigSpinner
.
Para prevenir que esto pase, puedes marcar la actualización del estado de navegación como una transición con startTransition
:
function Router() {
const [page, setPage] = useState('/');
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...
Esto le dice a React que la transición de estado no es urgente y que es mejor seguir mostrando la página anterior en lugar de esconder contenido ya revelado. Nota como al hacer clic el botón ahora «espera» a que se carga 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 transición no espera por que cargue todo el contenido. Espera lo suficiente para evitar esconder contenido ya revelado. Por ejemplo, la maquetación (definida por Layout
) del sitio ya había sido revelada, por lo que estaría mal esconderla dentro de un indicador de carga. Sin embargo, la barrera de Suspense
anidada alrededor de Albums
es nueva, por lo que la transición no espera por ella.
Indicar que está ocurriendo una transición
En el ejemplo de arriba, una vez que haces clic al botón, no hay un indicador visual de que hay una navegación en proceso. Para añadir un indicador, puedes reemplazar startTransition
con useTransition
que te da un valor booleano isPending
(que indica si la transición está pendiente). En el ejemplo de abajo, se usa para cambiar el estilo del encabezado del sitio mientras ocurre la transición:
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>; }
Reiniciar las barreras de Suspense al navegar
Durante una transición, React evitará esconder el contenido que ya ha sido revelado. Sin embargo, si navegas a una ruta con distintos parámetros, querrías decirle a React que es un contenido diferente. Puedes expresar esto con una key
:
<ProfilePage key={queryParams.id} />
Imagina que estás navegando dentro de la página del perfil de un usuario, y algo se suspende. Si esa actualización se envuelve en una transición no activará el fallback para el contenido ya visible. Ese es el comportamiento esperado.
Sin embargo, imagina ahora que estás navegando entre dos perfiles de usuario distintos. En ese caso, tiene sentido mostrar el fallback. Por ejemplo, la línea de tiempo de un usuario es un contenido diferente a la línea de tiempo de otro usuario. Al especificar una key
, te aseguras de que React trate distintos perfiles de usuario como componentes diferente y reinicie las barreras de Suspense durante la navegación. Un framework de enrutamiento integrado con Suspense debería hacerlo automáticamente.
Proporcionar un fallback para los errores del servidor y el contenido exclusivo del servidor
Si utilizas una de las API de renderizado en el servidor con streaming (o un framework que depende de ellas), React también utilizará tus barreras de <Suspense>
para manejar errores en el servidor. Si un componente lanza un error en el servidor, React no abortará el renderizado en el servidor. Lo que hará será encontrar el componente <Suspense>
más cercano encima de este e incluirá su fallback (un spinner, por ejemplo) dentro del HTML generado en el servidor. El usuario verá un spinner en lugar de un error.
En el cliente, React intentará renderizar el mismo componente nuevamente. Si ocurre un error también en el cliente, React lanzará el error y mostrará la barrera de error más cercana. Sin embargo, si no ocurre un error en el cliente, React no le mostrará el error al usuario dado que el contenido eventualmente se le mostró al usuario satisfactoriamente.
Puedes usar esto para evitar que algunos componentes se rendericen en el servidor. Para lograrlo, lanza un error desde ellos en el entorno del servidor y envuélvelos en una barrera de <Suspense>
para reemplazar su HTML con fallbacks:
<Suspense fallback={<Loading />}>
<Chat />
</Suspense>
function Chat() {
if (typeof window === 'undefined') {
throw Error('Chat should only render on the client.');
}
// ...
}
El HTML del servidor incluirá el indicador de carga. Este será reemplazado por el componente Chat
en el cliente.
Solución de problemas
¿Cómo puedo evitar que la interfaz de usuario sea sustituida por un fallback durante una actualización?
Reemplazar la interfaz de usuario visible por una de reserva crea una experiencia de usuario discordante. Esto puede ocurrir cuando una actualización hace que un componente se suspenda, y la barrera de Suspense más cercana ya está mostrando contenido al usuario.
Para evitar que esto ocurra, marca la actualización como no urgente utilizando startTransition
. Durante una transición, React esperará hasta que se hayan cargado suficientes datos para evitar que aparezca un fallback no deseado:
function handleNextPageClick() {
// If this update suspends, don't hide the already displayed content
startTransition(() => {
setCurrentPage(currentPage + 1);
});
}
Esto evitará ocultar el contenido existente. Sin embargo, cualquier barrera Suspense
recién renderizada seguirá mostrando inmediatamente los fallbacks para evitar el bloqueo de la UI y dejar que el usuario vea el contenido a medida que esté disponible.
React sólo evitará los «fallbacks» no deseados durante las actualizaciones no urgentes. No retrasará un renderizado si es el resultado de una actualización urgente. Debes indicarlo con una API como startTransition
o useDeferredValue
.
Si tu router está integrado con Suspense, debería envolver sus actualizaciones en startTransition
automáticamente.