<Suspense>
<Suspense> alt elemanları yüklenene kadar bir alternatif (fallback) göstermenize olanak sağlar.
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>- Referans
- Usage
- İçerik yüklenirken bir fallback gösterme
- İçeriği tek seferde birlikte gösterme
- İç içe içeriği yüklendikçe açığa çıkarma
- Yeni içerik yüklenirken eski içeriği gösterme
- Zaten açığa çıkmış içeriğin gizlenmesini önleme
- Transition’ın gerçekleştiğini gösterme
- Navigasyon sırasında Suspense sınırlarını sıfırlama
- Sunucu hataları ve sadece istemcide olan içerik için bir fallback sağlama
- Waiting for a stylesheet to load
- Animating from Suspense content
- Waiting for a font to load
- Waiting for an image to load
- Coordinating fonts, images, and stylesheets
- Troubleshooting
Referans
<Suspense>
Props
-
children: Render etmek istediğiniz gerçek UI. Eğerchildrenrendering sırasında suspend olursa, Suspense boundaryfallbackrender etmeye geçer. -
fallback: Gerçek UI henüz yüklenmeyi bitirmediyse, onun yerine render edilecek alternatif UI. Herhangi bir geçerli React node kabul edilir; ancak pratikte fallback genellikle loading spinner veya skeleton gibi hafif bir placeholder view’dur.childrensuspend olduğunda Suspense otomatik olarakfallback’e geçer, data hazır olduğunda tekrarchildren’a döner. Eğerfallbackrendering sırasında suspend olursa, en yakın parent Suspense boundary’yi aktive eder. -
Experimental only optional
defer: Bir boolean.trueolduğunda, içlerinde hiçbir şey suspend olmasa bile React öncefallback’i gösterebilir vechildren’ı daha sonra render veya stream edebilir. Render edilmesi pahalı olan content için kullanın. Default değerifalse’tur.
Uyarılar
-
Suspense, data’nın bir Effect veya event handler içinde fetch edildiğini detect etmez. Yalnızca aşağıda listelenen durumlarda aktive olur.
-
React, ilk kez mount olamadan önce suspend olan render’lar için hiçbir state’i preserve etmez. Component yüklendiğinde, React suspended tree’yi sıfırdan yeniden render etmeyi dener.
-
Suspense tree için content gösteriyorken tree tekrar suspend olursa, buna neden olan update
startTransitionveyauseDeferredValuetarafından oluşturulmadığı sürecefallbacktekrar gösterilir. -
React, suspended content’i son reveal’dan itibaren ölçülen en fazla 300ms’de bir reveal eder. Bu pencere içinde ready olan boundary’ler tek tek değil, birlikte reveal edilir.
-
React, tekrar suspend olduğu için zaten visible olan content’i hide etmek zorunda kalırsa, content tree içindeki layout Effects’leri cleanup eder. Content tekrar gösterilmeye hazır olduğunda, React layout Effects’i tekrar çalıştırır. Bu, DOM layout’unu ölçen Effects’in content hidden durumdayken bunu yapmaya çalışmamasını sağlar.
-
React, Suspense ile entegre olan Streaming Server Rendering ve Selective Hydration gibi under-the-hood optimizasyonlar içerir. Daha fazla bilgi edinmek için mimari overview okuyun ve teknik konuşmayı izleyin.
-
React, Suspense ile entegre olan Streaming Server Rendering ve Selective Hydration gibi under-the-hood optimizasyonlar içerir. Daha fazla bilgi edinmek için mimari overview okuyun ve teknik konuşmayı izleyin.
Suspense boundary’yi ne aktive eder?
Bir Suspense boundary, content’ini reveal etmeden önce onun hazır olmasını bekler. Aşağıdakilerden herhangi biri, bir boundary’nin content’ini reveal etmesini engeller:
-
lazyile component code’unu lazy-load etmek. -
Server Components’tan stream edilen veya Suspense-enabled framework üzerinden yüklenen data dahil olmak üzere,
useile bir Promise okumak. -
<link rel="stylesheet">ve birprecedenceprop’u ile render edilen bir stylesheet’i yüklemek. React, stylesheet yüklenene kadar, bir timeout’a kadar boundary’yi block eder. Aşağıdaki örneğe bakın. -
Streaming server rendering sırasında büyük bir boundary’nin HTML’inin gelmesini beklemek. HTML göndermek zaman alır; bu yüzden yeterince content içeren bir boundary, içinde hiçbir şey suspend olmasa bile aktive olur. React, HTML geldikçe content’i reveal eder.
-
Canary only Font yüklemek. Suspense default olarak font’ları beklemez, ancak bir
<ViewTransition>update’i, text’in fallback font ile flash etmemesi için yeni font’ların yüklenmesini bir timeout’a kadar bekler. Aşağıdaki örneğe bakın. -
Canary only Image yüklemek. Suspense default olarak image’ları beklemez, ancak bir
<ViewTransition>update’i sırasında React, image yüklenene kadar, bir timeout’a kadar boundary’yi block eder.onLoadhandler eklemek belirli bir image’ı bu davranışın dışına çıkarır. Aşağıdaki örneğe bakın. -
Experimental only Bir
<Suspense defer>boundary’si içinde CPU-bound render work gerçekleştirmek.
Usage
İçerik yüklenirken bir fallback gösterme
Uygulamanızın herhangi bir parçasını bir Suspense sınırıyla sarabilirsiniz:
<Suspense fallback={<Loading />}>
<Albums />
</Suspense>React yükleniyor fallback’inizi alt elemanların ihtiyaç duyduğu tüm kod ve veriler yüklenene kadar gösterecektir.
Aşağıdaki örnekte, Albums bileşeni albümler listesini fetch ederken askıya alınır. Render etmeye hazır olana kadar, React fallback’i —sizin Loading bileşeniniz— göstermek için en yakın Suspense sınırını etkinleştirir. Sonra, veri yüklendiğinde, React Loading fallback’ini gizler ve Albums bileşenini verilerle render eder.
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>🌀 Yükleniyor...</h2>; }
Buna karşılık, bir Effect içinde olduğu gibi use dışında data fetch eden code, boundary’yi aktive etmez:
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>; }
During streaming server rendering, a boundary also activates while its HTML is still streaming in. With any streaming server rendering API, React sends the shell with the fallback first, then streams in each boundary’s HTML and swaps out its fallback as that content arrives. Press “Render the page” to watch the page stream in:
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 />); // The posts resolve after the shell has streamed, so React // streams their HTML in and swaps out the 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')); });
İçeriği tek seferde birlikte gösterme
Varsayılan olarak, Suspense içindeki tüm ağaç tek bir birim olarak ele alınır. Örneğin, eğer bu bileşenlerden sadece biri veri beklemek için askıya alınırsa, tümü birlikte yükleniyor göstergesiyle değiştirilecektir:
<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>Sonrasında, hepsi görüntülenmeye hazır olduğunda, hepsi birlikte tek seferde açığa çıkacaktır.
Aşağıdaki örnekte, hem Biography hem Albums veri fetch etmekte. Ancak, tek bir Suspense sınırı altında gruplandıkları için, bu bileşenler her zaman aynı anda “açığa çıkıyor”.
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>🌀 Yükleniyor...</h2>; }
Veri yükleyen bileşenler Suspense sınırının doğrudan alt elemanı olmak zorunda değildir. Örneğin, Biography ve Albums’ü yeni bir Details bileşenine taşıyabilirsiniz. Bu davranışı değiştirmez. Biography ve Albums en yakın ebeveyn Suspense sınırını paylaştığı için, açığa çıkışları birlikte koordine edilir.
<Suspense fallback={<Loading />}>
<Details artistId={artist.id} />
</Suspense>
function Details({ artistId }) {
return (
<>
<Biography artistId={artistId} />
<Panel>
<Albums artistId={artistId} />
</Panel>
</>
);
}İç içe içeriği yüklendikçe açığa çıkarma
Bir bileşen askıya alındığında, en yakın üst Suspense sınırı fallback’i gösterir. Bu, bir yükleme sekansı oluşturmak için birden fazla Suspense sınırını iç içe geçirebilmenizi sağlar. Her Suspense sınırının fallback’i, bir sonraki içerik seviyesi kullanılabilir hale geldikçe doldurulur. Örneğin, albüm listesine kendi fallback’ini verebilirsiniz:
<Suspense fallback={<BigSpinner />}>
<Biography />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums />
</Panel>
</Suspense>
</Suspense>Bu değişiklikle birlikte, Biography’i göstermek Albums’ün yüklenmesini “beklemek” zorunda değildir.
Sekans şu şekilde olacaktır:
- Eğer
Biographyhenüz yüklenmediyse,BigSpinnertüm içerik alanının yerine gösterilir. Biographyyüklemeyi bitirdiğinde,BigSpinneriçerikle yer değiştirilir.- Eğer
Albumshenüz yüklenmediyse,AlbumsGlimmerAlbumsve onun üst bileşeniPanelyerine gösterilir. - Son olarak,
Albumsyüklemeyi bitirdiğinde,AlbumsGlimmeryerine geçer.
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>🌀 Yükleniyor...</h2>; } function AlbumsGlimmer() { return ( <div className="glimmer-panel"> <div className="glimmer-line" /> <div className="glimmer-line" /> <div className="glimmer-line" /> </div> ); }
Suspense sınırları kullanıcı arayüzünüzün hangi parçalarının her zaman birlikte “açığa çıkması” gerektiğini ve hangi parçaların yükleme durumları sekansı içerisinde progresif olarak daha fazla içerik açığa çıkarması gerektiğini koordine etmenizi sağlar. Suspense sınırlarını uygulamanızın geri kalanını etkilemeden ağaç içerisinde herhangi bir yere ekleyebilir, taşıyabilir ya da silebilirsiniz.
Her bileşenin etrafına bir Suspense sınırı koymayın. Suspense sınırları kullanıcıların deneyimlemesini istediğiniz yükleme sekansından daha tanecikli olmamalıdır. Eğer bir tasarımcı ile çalışıyorsanız, yükleme durumlarının nereye konulması gerektiğini sorun—muhtemelen zaten tasarım wireframe’lerine dahil etmişlerdir.
Yeni içerik yüklenirken eski içeriği gösterme
Bu örnekte, SearchResults bileşeni arama sonuçlarını fetch ederken askıya alınır. "a" Yazın, sonuçları bekleyin ve daha sonra yazıyı "ab" olarak düzenleyin. "a" için gelen sonuçlar yükleme fallback’i ile değiştirilecektir.
import { Suspense, useState } from 'react'; import SearchResults from './SearchResults.js'; export default function App() { const [query, setQuery] = useState(''); return ( <> <label> Albümleri ara: <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Yükleniyor...</h2>}> <SearchResults query={query} /> </Suspense> </> ); }
Yaygın bir alternatif UI pattern’i, list’i update etmeyi defer etmek ve yeni sonuçlar hazır olana kadar önceki sonuçları göstermeye devam etmektir. useDeferredValue Hook’u, query’nin deferred bir version’ını aşağıya pass etmenizi sağlar:
export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Albümleri ara:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Yükleniyor...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}sorgu (query) hemen güncellenecektir, bu yüzden girdi yeni değeri gösterecektir. Ancak, deferredQuery veri yüklenene kadar önceki değerini koruyacaktır, bu yüzden SearchResults bir süreliğine eski sonuçları gösterecektir.
Kullanıcıya daha belli etmek için, eski sonuç listesinin gösterildiği zamanlarda görsel bir gösterge ekleyebilirsiniz:
<div style={{
opacity: query !== deferredQuery ? 0.5 : 1
}}>
<SearchResults query={deferredQuery} />
</div>Aşağıdaki örneğe "a" yazın, sonuçların yüklenmesini bekleyin, sonrasında girdiyi "ab" olarak değiştirin. Yeni sonuçlar yüklenene kadar Suspense fallback’i yerine soluklaşmış eski sonuç listesini gördüğünüze dikkat edin:
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> Albümleri ara: <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Yükleniyor...</h2>}> <div style={{ opacity: isStale ? 0.5 : 1 }}> <SearchResults query={deferredQuery} /> </div> </Suspense> </> ); }
Zaten açığa çıkmış içeriğin gizlenmesini önleme
Bir bileşen askıya alındığında, en yakın Suspense sınırı fallback’i göstermeye geçer. Bu, zaten bir içerik gösteriliyorsa uyumsuz bir kullanıcı deneyimine yol açabilir. Bu düğmeye basmayı deneyin:
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>🌀 Yükleniyor...</h2>; }
Butona bastığınızda Router bileşeni ArtistPage sayfası yerine IndexPage sayfasını render etti. ArtistPage içerisindeki bir bileşen askıya alındı, bu yüzden en yakın Suspense sınırı fallback’i göstermeye başladı. En yakın Suspense sınırı köke yakındı, bu yüzden tüm site layout’u BigSpinner ile değiştirildi.
Bunu engellemek için, navigasyon state güncellemesini bir geçiş (transition) olarak startTransition: ile işaretleyebilirsiniz:
function Router() {
const [page, setPage] = useState('/');
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...Bu, React’e state transition’ının acil olmadığını, ve zaten açığa çıkmış içeriği gizlemek yerine önceki sayfayı göstermeye devam etmenin daha iyi olduğunu söyler. Şimdi butona basmak Biography’nin yüklenmesini “bekler”:
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>🌀 Yükleniyor...</h2>; }
Bir transition tüm içeriğin yüklenmesini beklemez. Zaten açığa çıkmış içeriği gizlemekten kaçınmak için ne kadar beklemesi gerekiyorsa o kadar bekler. Örneğin, web sitesinin Layout’u zaten açığa çıkmıştı, bu yüzden onu bir yükleniyor çarkının arkasına saklamak kötü olurdu. Bununla birlikte, Albums’ün etrafındaki iç içe geçmiş Suspense sınırı yeni olduğundan, transition onu beklemiyor.
Transition’ın gerçekleştiğini gösterme
Yukarıdaki örnekte, butona bastığınızda navigasyonun gerçekleştiğini gösteren bir görsel gösterge bulunmamakta. Bir gösterge eklemek için, startTransition’ı useTransition ile değiştirebilirsiniz, bu size bir boolean olan isPending değerini verecektir. Aşağıdaki örnekte, transition’ın gerçekleştiği sırada web sitesi başlığı stilini değiştirmek için useTransition kullanılmıştır:
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>🌀 Yükleniyor...</h2>; }
Navigasyon sırasında Suspense sınırlarını sıfırlama
Bir Transition sırasında React, zaten reveal edilmiş content’i hide etmekten kaçınır. Ancak başka bir kullanıcının profili gibi farklı content’e navigate ettiğinizde, boundary’nin önceki content yerine fallback göstermesini istersiniz. Bunu bir key ile ifade edebilirsiniz:
<ProfilePage key={queryParams.id} />Farklı bir key ile React, profile’ları farklı content olarak ele alır ve navigation sırasında Suspense boundary’yi resetler. key, boundary’nin kendisine veya onun üstündeki bir component’e verilebilir. Suspense-integrated router’lar bunu otomatik olarak yapmalıdır.
Aşağıdaki örnekte, profile sayfasını açmak ilk profile’ı yükler. “Bob”a basmak farklı bir profile’a navigate eder ve key boundary’yi resetlediği için önceki kullanıcının bio’su yerine fallback gösterilir. key’i kaldırmayı deneyin: bir sonraki yüklenirken önceki bio görünür kalır:
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> </> ); }
Sunucu hataları ve sadece istemcide olan içerik için bir fallback sağlama
Eğer stream’leyen sunucu render etme API’lerinden birini (ya da onlara bağlı bir framework) kullanıyorsanız, React sunucuda hataları ele almak için <Suspense> sınırlarınızı kullanacaktır. Eğer bir bileşen sunucuda bir hata throw ederse, React sunucu render’ını iptal etmeyecektir. Bunun yerine, onun üzerindeki en yakın <Suspense> bileşenini bulacak ve oluşturulan sunucu HTML’ine bileşenin fallback’ini (örneğin bir yükleniyor çarkı) dahil edecektir. Kullanıcı ilk olarak bir yükleniyor çarkı görecektir.
İstemci tarafında (client), React aynı bileşeni yeniden render etmeye çalışır. Eğer istemci tarafında da hata oluşursa, React bu hatayı fırlatır (throw) ve en yakın Error Boundary bileşenini gösterir. Ancak, istemci tarafında hata oluşmazsa, React kullanıcıya hatayı göstermez; çünkü içerik sonuçta başarılı bir şekilde görüntülenmiştir.
Bunu bazı bileşenlerin sunucuda yüklenmemesini sağlamak için kullanabilirsiniz. Bunu yapmak için, sunucu ortamında bir hata throw edin ve ardından HTML’lerini fallback’lerle değiştirmek için <Suspense> sınırı içine alın:
<Suspense fallback={<Loading />}>
<Chat />
</Suspense>
function Chat() {
if (typeof window === 'undefined') {
throw Error('Chat bileşeni sadece istemcide render edilmelidir.');
}
// ...
}Sunucu HTML’i yükleniyor çarkını içerecektir. İstemci tarafında yükleniyor çarkı Chat bileşeni ile değiştirilecektir.
Waiting for a stylesheet to load
<link rel="stylesheet"> ve bir precedence prop’u ile render edilen bir stylesheet, content’in unstyled görünmemesi için stylesheet yüklenene kadar, bir timeout’a kadar Suspense boundary’yi block eder.
Aşağıdaki örnekte, Card component’i precedence ile bir stylesheet render eder. “Show card”a basın: React, stylesheet yüklenene kadar fallback’i gösterir ve ardından card’ı style’ları uygulanmış şekilde reveal eder.
Karşılaştırma için, ikinci button aynı update’i React olmadan, ayrı bir document içinde gerçekleştirir. Hiçbir şey stylesheet’i beklemez, bu yüzden card’ın text’i önce fallback font ile görünür ve ardından değişir:
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 /> </> ); }
Canary only Animating from Suspense content
Suspense composes with <ViewTransition> to animate the swap from the fallback to the content. Wrap the boundary in a <ViewTransition>, and React treats the swap as an update, cross-fading between the fallback and the content by default:
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} </> ); }
Canary only Waiting for a font to load
When a <ViewTransition> animates a Suspense boundary’s reveal, React waits for new fonts the content introduces, up to a timeout, so the text doesn’t flash with a fallback font. This only happens during a <ViewTransition> update.
In the example below, the Suspense boundary is wrapped in a <ViewTransition>, and the Quote component suspends while its data loads. Rendering the quote starts its font download. React keeps the fallback visible until the font has loaded, so the quote appears already in its font.
For comparison, the second button performs the same update without React. Nothing waits for the font, so the text appears in a fallback font first and then switches:
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 /> </> ); }
Canary only Waiting for an image to load
When a <ViewTransition> animates a Suspense boundary’s reveal, React waits for visible images to load, up to a timeout, so the animation doesn’t start with a half-loaded image. This only happens during a <ViewTransition> update. Adding an onLoad handler opts a specific image out, even inside a <ViewTransition>.
In the example below, the Suspense boundary is wrapped in a <ViewTransition> and shows a profile skeleton until the portrait has loaded.
For comparison, the second button performs the same update without React. Nothing waits for the image, so the card appears immediately and the image pops in when it loads:
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"> </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 /> </> ); }
Canary only Coordinating fonts, images, and stylesheets
A Suspense boundary can wait for data, stylesheets, fonts, and images at once. Waiting for fonts and images only happens during a <ViewTransition> update. In the example below, the ProfileCard component suspends while its data loads, and renders a stylesheet with precedence, text in a new font, and a portrait. React keeps the skeleton visible while the data and the stylesheet load. The <ViewTransition> reveal then waits for the font and the image, so the card appears complete.
For comparison, the version without React loads the same data and shows every resource arriving on its own schedule:
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"> </p> <p className="bio bio-placeholder"> </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
Kullanıcı arayüzünün bir güncelleme sırasında bir fallback ile değiştirilmesini nasıl engellerim?
Görünür bir kullanıcı arayüzünü bir fallback ile değiştirmek, uyumsuz bir kullanıcı deneyimine sebep olur. Bu, bir güncelleme bir bileşenin askıya alınmasına sebep olduğunda ve en yakın Suspense sınırı zaten kullanıcıya içerik gösteriyorsa olabilir.
Bunun olmasını engellemek için, güncellemeyi startTransition ile acil olmayan olarak işaretleyin. Bir transition sırasında, React istenmeyen bir fallback’in görünmesini engellemek için yeterli veri yüklenene kadar bekleyecektir:
function handleNextPageClick() {
// Eğer bu güncelleme askıya alınırsa, zaten görünen içeriği gizleme
startTransition(() => {
setCurrentPage(currentPage + 1);
});
}Bu, varolan içeriği gizlemeyi önleyecektir. Ancak, yeni render edilen Suspense sınırları hala kullanıcı arayüzünü bloke etmemek ve kullanıcının içeriği hazır hale geldikçe görmesini sağlamak için hemen fallback gösterecektir.
React sadece istenmeyen fallback’leri acil olmayan güncellemeler sırasında engeller. Eğer acil bir güncelleme sonucunda gerçekleşiyorsa, bir render’ı geciktirmeyecektir. startTransition veya useDeferredValue gibi bir API tercih etmeniz gerekecektir.
Eğer router’ınız Suspense ile entegre ise, güncellemelerini startTransition’ın içerisine otomatik olarak sarması gerekmektedir.