<Suspense>
<Suspense>
permite exibir um fallback até que seus filhos terminem de carregar.
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>
- Referência
- Uso
- Exibindo um fallback enquanto o conteúdo está carregando
- Revelando o conteúdo juntos de uma vez
- Revelando conteúdo aninhado à medida que ele carrega
- Mostrando conteúdo obsoleto enquanto o conteúdo novo está carregando
- Prevenindo que o conteúdo já revelado se esconda
- Indicando que uma Transition está acontecendo
- Resetando limites de Suspense na navegação
- Fornecendo um fallback para erros no servidor e conteúdo somente do cliente
- Solução de problemas
Referência
<Suspense>
Props
children
: A UI real que você pretende renderizar. Sechildren
suspender durante a renderização, o limite do Suspense mudará para renderizarfallback
.fallback
: Uma UI alternativa para renderizar no lugar da UI real se ela não tiver terminado de carregar. Qualquer nó React válido é aceito, embora na prática, um fallback seja uma visualização de espaço reservado leve, como um indicador de carregamento ou esqueleto. Suspense mudará automaticamente parafallback
quandochildren
suspender, e de volta parachildren
quando os dados estiverem prontos. Sefallback
suspender durante a renderização, ele ativará o limite do Suspense pai mais próximo.
Ressalvas
- React não preserva nenhum estado para renderizações que foram suspensas antes de poderem montar pela primeira vez. Quando o componente tiver carregado, React tentará renderizar a árvore suspensa do zero.
- Se Suspense estava exibindo conteúdo para a árvore, mas então suspendeu novamente, o
fallback
será mostrado novamente, a menos que a atualização que o causou tenha sido causada porstartTransition
ouuseDeferredValue
. - Se React precisar ocultar o conteúdo já visível porque ele suspendeu novamente, ele limpará os Effects de layout na árvore de conteúdo. Quando o conteúdo estiver pronto para ser mostrado novamente, React executará os Effects de layout novamente. Isso garante que os Effects que medem o layout do DOM não tentem fazer isso enquanto o conteúdo estiver oculto.
- React inclui otimizações internas como Streaming Server Rendering e Selective Hydration que são integradas ao Suspense. Leia uma visão geral da arquitetura e assista a uma palestra técnica para saber mais.
Uso
Exibindo um fallback enquanto o conteúdo está carregando
Você pode encapsular qualquer parte de sua aplicação com um limite do Suspense:
<Suspense fallback={<Loading />}>
<Albums />
</Suspense>
React exibirá seu fallback de carregamento até que todo o código e dados necessários para os filhos tenham sido carregados.
No exemplo abaixo, o componente Albums
suspende enquanto busca a lista de álbuns. Até que esteja pronto para renderizar, React muda o limite de Suspense mais próximo acima para mostrar o fallback — seu componente Loading
. Então, quando os dados carregam, React oculta o fallback Loading
e renderiza o componente Albums
com dados.
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>; }
Revelando o conteúdo juntos de uma vez
Por padrão, toda a árvore dentro de Suspense é tratada como uma única unidade. Por exemplo, mesmo que apenas um desses componentes suspenda esperando por alguns dados, todos eles juntos serão substituídos pelo indicador de carregamento:
<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>
Então, depois que todos estiverem prontos para serem exibidos, todos aparecerão juntos de uma vez.
No exemplo abaixo, tanto Biography
quanto Albums
buscam alguns dados. No entanto, como eles estão agrupados sob um único limite de Suspense, esses componentes sempre “aparecem” juntos ao mesmo tempo.
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>; }
Componentes que carregam dados não precisam ser filhos diretos do limite do Suspense. Por exemplo, você pode mover Biography
e Albums
para um novo componente Details
. Isso não muda o comportamento. Biography
e Albums
compartilham o mesmo limite de Suspense pai mais próximo, então a revelação deles é coordenada em conjunto.
<Suspense fallback={<Loading />}>
<Details artistId={artist.id} />
</Suspense>
function Details({ artistId }) {
return (
<>
<Biography artistId={artistId} />
<Panel>
<Albums artistId={artistId} />
</Panel>
</>
);
}
Revelando conteúdo aninhado à medida que ele carrega
Quando um componente suspende, o componente Suspense pai mais próximo mostra o fallback. Isso permite que você aninhe vários componentes Suspense para criar uma sequência de carregamento. O fallback de cada limite de Suspense será preenchido à medida que o próximo nível de conteúdo se torna disponível. Por exemplo, você pode dar à lista de álbuns seu próprio fallback:
<Suspense fallback={<BigSpinner />}>
<Biography />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums />
</Panel>
</Suspense>
</Suspense>
Com essa alteração, exibir a Biography
não precisa “esperar” o carregamento de Albums
.
A sequência será:
- Se a
Biography
ainda não tiver carregado, oBigSpinner
será exibido no lugar de toda a área de conteúdo. - Depois que a
Biography
terminar de carregar, oBigSpinner
será substituído pelo conteúdo. - Se
Albums
ainda não tiver carregado,AlbumsGlimmer
será exibido no lugar deAlbums
e seu paiPanel
. - Finalmente, depois que
Albums
terminar de carregar, ele substituirá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> ); }
Limites do Suspense permitem que você coordene quais partes da sua UI devem sempre “aparecer” juntas ao mesmo tempo e quais partes devem revelar progressivamente mais conteúdo em uma sequência de estados de carregamento. Você pode adicionar, mover ou deletar limites de Suspense em qualquer lugar na árvore sem afetar o comportamento do restante do seu aplicativo.
Não coloque um limite de Suspense em todo componente. Os limites de Suspense não devem ser mais granulares do que a sequência de carregamento que você deseja que o usuário experimente. Se você trabalhar com um designer, pergunte a ele onde os estados de carregamento devem ser colocados - é provável que eles já os tenham incluído em seus wireframes de design.
Mostrando conteúdo obsoleto enquanto o conteúdo novo está carregando
Neste exemplo, o componente SearchResults
suspende enquanto busca os resultados da pesquisa. Digite "a"
, espere os resultados e, em seguida, edite-o para "ab"
. Os resultados de "a"
serão substituídos pelo fallback de carregamento.
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> </> ); }
Uma alternativa comum para o padrão de UI é adiar a atualização da lista e continuar mostrando os resultados anteriores até que os novos resultados estejam prontos. O Hook useDeferredValue
permite que você passe uma versão adiada da 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>
</>
);
}
O query
atualizará imediatamente, então a entrada exibirá o novo valor. No entanto, o deferredQuery
manterá seu valor anterior até que os dados sejam carregados, então o SearchResults
mostrará os resultados obsoletos por um tempo.
Para tornar isso mais óbvio para o usuário, você pode adicionar uma indicação visual quando a lista de resultados obsoletos for exibida:
<div style={{
opacity: query !== deferredQuery ? 0.5 : 1
}}>
<SearchResults query={deferredQuery} />
</div>
Digite "a"
no exemplo abaixo, espere os resultados carregarem e, em seguida, edite a entrada para "ab"
. Observe como, em vez do fallback do Suspense, você agora vê a lista de resultados obsoletos atenuados até que os novos resultados sejam carregados:
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> </> ); }
Prevenindo que o conteúdo já revelado se esconda
Quando um componente suspende, o limite de Suspense pai mais próximo muda para mostrar o fallback. Isso pode levar a uma experiência do usuário desagradável se ele já estiver exibindo algum conteúdo. Tente pressionar este botão:
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>🌀 Carregando...</h2>; }
Quando você pressionou o botão, o componente Router
renderizou ArtistPage
em vez de IndexPage
. Um componente dentro de ArtistPage
suspendeu, então, o limite de Suspense mais próximo começou a mostrar o fallback. O limite de Suspense mais próximo estava perto da raiz, então todo o layout do site foi substituído por BigSpinner
.
Para evitar isso, você pode marcar a atualização de estado da navegação como uma Transition com startTransition
:
function Router() {
const [page, setPage] = useState('/');
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...
Isso informa ao React que a transição de estado não é urgente e é melhor continuar mostrando a página anterior em vez de ocultar qualquer conteúdo já revelado. Agora, clicar no botão “espera” o carregamento da 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>🌀 Carregando...</h2>; }
Uma Transição não espera que todo o conteúdo seja carregado. Ela espera apenas o tempo suficiente para evitar a ocultação do conteúdo já revelado. Por exemplo, o Layout
do site já foi revelado, então seria ruim escondê-lo atrás de um spinner de carregamento. No entanto, o limite de Suspense
aninhado em torno de Albums
é novo, então a Transição não espera por ele.
Indicando que uma Transition está acontecendo
No exemplo acima, assim que você clica no botão, não há nenhuma indicação visual de que uma navegação está em andamento. Para adicionar um indicador, você pode substituir startTransition
por useTransition
que fornece um valor booleano isPending
. No exemplo abaixo, ele é usado para alterar o estilo do cabeçalho do site enquanto uma Transition está acontecendo:
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>🌀 Carregando...</h2>; }
Resetando limites de Suspense na navegação
Durante uma Transition, o React evitará ocultar o conteúdo já revelado. No entanto, se você navegar para uma rota com parâmetros diferentes, você pode querer dizer ao React que é um conteúdo diferente. Você pode expressar isso com uma key
:
<ProfilePage key={queryParams.id} />
Imagine que você está navegando dentro da página de perfil de um usuário e algo suspende. Se essa atualização estiver envolvida em uma Transition, ela não acionará o fallback para o conteúdo já visível. Esse é o comportamento esperado.
No entanto, agora imagine que você está navegando entre dois perfis de usuário diferentes. Nesse caso, faz sentido mostrar o fallback. Por exemplo, a linha do tempo de um usuário é um conteúdo diferente da linha do tempo de outro usuário. Especificando uma key
, você garante que o React trate os perfis de usuários diferentes como componentes diferentes e redefine os limites de Suspense durante a navegação. Roteadores integrados ao Suspense devem fazer isso automaticamente.
Fornecendo um fallback para erros no servidor e conteúdo somente do cliente
Se você usar uma das APIs de renderização no servidor de streaming (ou um framework que dependa delas), React também usará seus limites de <Suspense>
para lidar com erros no servidor. Se um componente lançar um erro no servidor, o React não abortará a renderização do servidor. Em vez disso, ele encontrará o componente <Suspense>
mais próximo acima dele e incluirá seu fallback (como um spinner) no HTML do servidor gerado. O usuário verá um spinner no início.
No cliente, o React tentará renderizar o mesmo componente novamente. Se ele também gerar erros no cliente, o React lançará o erro e exibirá o limite de erro mais próximo. No entanto, se não gerar erros no cliente, o React não exibirá o erro ao usuário, pois o conteúdo foi exibido com sucesso.
Você pode usar isso para excluir alguns componentes da renderização no servidor. Para fazer isso, lance um erro no ambiente do servidor e, em seguida, envolva-os em um limite de <Suspense>
para substituir seu HTML por fallbacks:
<Suspense fallback={<Loading />}>
<Chat />
</Suspense>
function Chat() {
if (typeof window === 'undefined') {
throw Error('Chat should only render on the client.');
}
// ...
}
O HTML do servidor incluirá o indicador de carregamento. Ele será substituído pelo componente Chat
no cliente.
Solução de problemas
Como evito que a UI seja substituída por um fallback durante uma atualização?
Substituir a UI visível por um fallback cria uma experiência do usuário desagradável. Isso pode acontecer quando uma atualização faz com que um componente suspenda, e o limite de Suspense mais próximo já está mostrando conteúdo ao usuário.
Para evitar que isso aconteça, marque a atualização como não urgente usando startTransition
. Durante uma Transition, o React aguardará até que dados suficientes tenham sido carregados para impedir o aparecimento de um fallback indesejado:
function handleNextPageClick() {
// Se esta atualização suspender, não oculte o conteúdo já exibido
startTransition(() => {
setCurrentPage(currentPage + 1);
});
}
Isso evitará ocultar o conteúdo existente. No entanto, quaisquer limites de Suspense
recém-renderizados ainda exibirão imediatamente fallbacks para evitar bloquear a UI e permitir que o usuário veja o conteúdo conforme ele se torna disponível.
O React só evitará fallbacks indesejados durante atualizações não urgentes. Ele não atrasará uma renderização se for o resultado de uma atualização urgente. Você deve aceitar com uma API como startTransition
ou useDeferredValue
.
Se seu roteador estiver integrado ao Suspense, ele deverá envolver suas atualizações em startTransition
automaticamente.