<Suspense>
<Suspense>
memungkinkan Anda menampilkan fallback hingga komponen anak-anaknya selesai dimuat.
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>
- Referensi
- Penggunaan
- Menampilkan fallback saat konten sedang dimuat
- Menampilkan konten secara bersamaan sekaligus
- Menunjukkan konten yang tersusun saat dimuat
- Menampilkan konten yang sudah usang saat konten baru sedang dimuat
- Mencegah konten yang sudah ditunjukkan agar tidak disembunyikan
- Mengindikasikan bahwa transisi sedang terjadi
- Menyetel ulang batasan Suspense pada navigasi
- Menyediakan fallback untuk kesalahan server dan konten khusus klien
- Pemecahan Masalah
Referensi
<Suspense>
Props
children
: UI sebenarnya yang ingin Anda render. Jikachildren
ditangguhkan saat di-render, maka Suspense akan beralih me-renderfallback
.fallback
: UI alternatif untuk di-render menggantikan UI yang sebenarnya jika belum selesai dimuat. Simpul React apapun yang valid akan diterima, meskipun dalam praktiknya, fallback adalah tampilan pengganti yang ringan, Suspense akan secara otomatis beralih kefallback
ketikachildren
ditangguhkan, dan kembali kechildren
ketika datanya sudah siap. Jikafallback
ditangguhkan sewaktu melakukan rendering, itu akan mengaktifkan batasan Suspense dari induk terdekat.
Catatan Penting
- React tidak menyimpan state apa pun untuk render-an yang ditangguhkan sebelum dapat dimuat untuk pertama kalinya. Ketika komponen sudah dimuat, React akan mencoba me-render ulang komponen yang ditangguhkan dari awal.
- Jika Suspense menampilkan konten untuk komponen, namun kemudian ditangguhkan lagi,
fallback
akan ditampilkan kembali kecuali jika pembaruan yang menyebabkannya, disebabkan olehstartTransition
atauuseDeferredValue
. - Jika React perlu menyembunyikan konten yang sudah terlihat karena ditangguhkan kembali, React akan membersihkan layout Effects yang ada di dalam konten komponen. Ketika konten siap untuk ditampilkan lagi, React akan menjalankan layout Effects lagi. Hal ini memastikan bahwa Efek yang mengukur tata letak DOM tidak mencoba melakukan hal ini saat konten disembunyikan.
- React menyertakan pengoptimalan di balik layar seperti Streaming Server Rendering dan Selective Hydration yang terintegrasi dengan Suspense. Baca tinjauan arsitektur dan tonton diskusi teknis untuk mempelajari lebih lanjut.
Penggunaan
Menampilkan fallback saat konten sedang dimuat
Anda dapat membungkus bagian mana pun dari aplikasi Anda dengan batasan Suspense:
<Suspense fallback={<Loading />}>
<Albums />
</Suspense>
React akan menampilkan fallback pemuatan hingga semua kode dan data yang dibutuhkan oleh anak-anaknya telah selesai dimuat.
Pada contoh di bawah ini, komponen Albums
ditangguhkan saat mengambil daftar album. Hingga komponen tersebut siap untuk di-render, React mengganti batasan Suspense terdekat di atas untuk menunjukkan fallback—komponen Loading
Anda. Kemudian, saat data termuat, React menyembunyikan fallback Loading
dan me-render komponen Albums
dengan data.
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>🌀 Memuat...</h2>; }
Menampilkan konten secara bersamaan sekaligus
Secara default, seluruh pohon di dalam Suspense diperlakukan sebagai satu kesatuan. Sebagai contoh, meskipun hanya satu dari komponen-komponen ini yang tertahan menunggu beberapa data, semua komponen tersebut akan digantikan oleh indikator pemuatan:
<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>
Kemudian, setelah semuanya siap untuk ditampilkan, semuanya akan muncul sekaligus.
Pada contoh di bawah ini, baik Biography
dan Album
mengambil beberapa data. Namun, karena mereka dikelompokkan di bawah satu batasan Suspense, komponen-komponen ini akan selalu “muncul” bersama-sama pada waktu yang sama.
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>🌀 Memuat...</h2>; }
Komponen yang memuat data tidak harus menjadi anak langsung dari batasan Suspense. Sebagai contoh, Anda dapat memindahkan Biography
dan Album
ke dalam komponen Details
yang baru. Hal ini tidak akan mengubah perilakunya. Biography
dan Albums
memiliki batasan Suspense induk terdekat yang sama, sehingga pemunculannya dikoordinasikan bersama-sama.
<Suspense fallback={<Loading />}>
<Details artistId={artist.id} />
</Suspense>
function Details({ artistId }) {
return (
<>
<Biography artistId={artistId} />
<Panel>
<Albums artistId={artistId} />
</Panel>
</>
);
}
Menunjukkan konten yang tersusun saat dimuat
Ketika sebuah komponen ditangguhkan, komponen Suspense induk terdekat akan menampilkan fallback. Hal ini memungkinkan Anda menyatukan beberapa komponen Suspense untuk membuat pemuatan terurut. Fallback setiap batasan Suspense akan terisi saat level konten berikutnya tersedia. Sebagai contoh, Anda dapat memberikan daftar album sebuah fallback-nya sendiri:
<Suspense fallback={<BigSpinner />}>
<Biography />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums />
</Panel>
</Suspense>
</Suspense>
Dengan perubahan ini, menampilkan Biography
tidak perlu “menunggu” hingga Album
termuat.
Urutan pemuatannya adalah sebagai berikut:
- Jika
Biography
belum dimuat,BigSpinner
ditampilkan sebagai pengganti seluruh area konten. - Setelah
Biography
selesai dimuat,BigSpinner
digantikan oleh konten. - Jika
Albums
belum dimuat,AlbumsGlimmer
ditampilkan sebagai penggantiAlbums
dan induknyaPanel
. - Akhirnya, setelah
Albums
selesai dimuat, dia akan menggantikanAlbumsGlimmer
.
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>🌀 Memuat...</h2>; } function AlbumsGlimmer() { return ( <div className="glimmer-panel"> <div className="glimmer-line" /> <div className="glimmer-line" /> <div className="glimmer-line" /> </div> ); }
Batasan Suspense memungkinkan Anda mengkoordinasikan bagian dari UI Anda yang harus selalu “muncul” bersamaan, dan bagian yang harus menampilkan lebih banyak konten secara bertahap dalam urutan status pemuatan. Anda dapat menambah, memindahkan, atau menghapus batasan-batasan Suspense di mana saja di dalam pohon tanpa mempengaruhi perilaku bagian lainnya dari aplikasi Anda.
Jangan memberikan batasan Suspense pada setiap komponen. Batas suspense tidak boleh lebih spesifik daripada urutan pemuatan yang Anda inginkan untuk pengalman pengguna. Jika Anda bekerja dengan desainer, tanyakan kepada mereka di mana status pemuatan harus ditempatkan, mungkin mereka sudah memasukkannya dalam wireframe desain mereka.
Menampilkan konten yang sudah usang saat konten baru sedang dimuat
Dalam contoh ini, komponen SearchResults
ditangguhkan saat sedang mengambil hasil pencarian. Ketik "a"
, tunggu hasilnya, dan kemudian ubah menjadi "ab"
. Hasil untuk "a"
akan tergantikan oleh fallback pemuatan.
import { Suspense, useState } from 'react'; import SearchResults from './SearchResults.js'; export default function App() { const [query, setQuery] = useState(''); return ( <> <label> Cari album: <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Memuat...</h2>}> <SearchResults query={query} /> </Suspense> </> ); }
Pola UI alternatif yang umum adalah dengan menangguhkan pembaruan daftar dan tetap menampilkan hasil sebelumnya hingga hasil yang baru siap. Hook useDeferredValue
memungkinkan Anda untuk memberikan versi yang ditangguhkan dari kueri:
export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Cari album:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Memuat...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}
query
akan segera diperbarui, sehingga input akan menampilkan nilai baru. Namun, deferredQuery
akan menyimpan nilai sebelumnya sampai data dimuat, sehingga SearchResults
akan menunjukkan hasil yang sebelumnya untuk sementara waktu.
Untuk membuatnya lebih jelas bagi pengguna, Anda bisa menambahkan indikasi visual apabila daftar hasil yang sudah usang ditampilkan:
<div style={{
opacity: query !== deferredQuery ? 0.5 : 1
}}>
<SearchResults query={deferredQuery} />
</div>
Masukkan "a"
didalam contoh berikut ini, tunggu hingga hasilnya dimuat, lalu ubah masukan ke "ab"
. Perhatikan, bahwa alih-alih menampilkan fallback Suspense, Anda sekarang melihat daftar hasil sebelumnya yang diredupkan sampai hasil yang baru dimuat:
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> Cari album: <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Memuat...</h2>}> <div style={{ opacity: isStale ? 0.5 : 1 }}> <SearchResults query={deferredQuery} /> </div> </Suspense> </> ); }
Mencegah konten yang sudah ditunjukkan agar tidak disembunyikan
Ketika sebuah komponen ditangguhkan, batasan Suspense induk terdekat akan beralih untuk menampilkan fallback. Hal ini dapat menyebabkan pengalaman pengguna yang mengejutkan jika komponen tersebut sudah menampilkan beberapa konten. Coba tekan tombol ini:
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>🌀 Memuat...</h2>; }
Saat Anda menekan tombol, komponen Router
akan me-render ArtistPage
, bukan IndexPage
. Komponen di dalam ArtistPage
ditangguhkan, sehingga batasan Suspense terdekat mulai menampilkan fallback. Batasan Suspense terdekat berada pada dekat akar, sehingga seluruh tata letak situs digantikan oleh BigSpinner
.
Untuk mencegah hal ini, Anda dapat menandai pembaruan status navigasi sebagai transition dengan startTransition
:
function Router() {
const [page, setPage] = useState('/');
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...
Dengan begitu, React diberi tahu bahwa transisi state tidak mendesak, dan lebih baik tetap menampilkan halaman sebelumnya daripada menyembunyikan konten yang sudah ditampilkan. Sekarang pengklikan tombol akan “menunggu” sampai Biography
dimuat:
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>🌀 Memuat...</h2>; }
Transisi tidak menunggu semua konten dimuat. Transisi hanya menunggu cukup lama untuk menghindari menyembunyikan konten yang sudah ditunjukkan. Misalnya, situs web Layout
sudah ditunjukkan, jadi tidak baik menyembunyikannya di balik loading spinner. Namun, batasan Suspense
yang ada di sekitar Albums
adalah Suspense yang baru, jadi transisinya tidak perlu ditunggu.
Mengindikasikan bahwa transisi sedang terjadi
Pada contoh di atas, setelah Anda mengeklik tombol, tidak ada indikasi visual bahwa navigasi sedang berlangsung. Untuk menambahkan indikator, Anda dapat mengganti startTransition
dengan useTransition
yang akan memberi Anda nilai boolean isPending
. Pada contoh di bawah ini, nilai tersebut digunakan untuk mengubah gaya header situs web saat transisi terjadi:
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>🌀 Memuat...</h2>; }
Menyetel ulang batasan Suspense pada navigasi
Selama transisi, React akan menghindari menyembunyikan konten yang sudah ditunjukkan. Namun, jika Anda menavigasi ke rute dengan parameter yang berbeda, Anda mungkin ingin memberi tahu React bahwa itu adalah konten yang berbeda. Anda dapat mengekspresikan ini dengan sebuah key
:
<ProfilePage key={queryParams.id} />
Bayangkan Anda sedang menavigasi dalam halaman profil pengguna, dan ada sesuatu yang ditangguhkan. Jika pembaruan itu dibungkus dengan transisi, pembaruan itu tidak akan menampilkan fallback untuk konten yang sudah terlihat. Itulah perilaku yang diharapkan.
Namun, sekarang bayangkan Anda menavigasi di antara dua profil pengguna yang berbeda. Dalam kasus ini, masuk akal untuk menampilkan fallback. Sebagai contoh, timeline salah satu pengguna adalah konten yang berbeda dengan timeline pengguna lain. Dengan menentukan sebuah kunci
, Anda memastikan bahwa React memperlakukan profil pengguna yang berbeda sebagai komponen yang berbeda, dan menyetel ulang batasan-batasan Suspense selama navigasi. Router yang terintegrasi dengan Suspense seharusnya melakukan ini secara otomatis.
Menyediakan fallback untuk kesalahan server dan konten khusus klien
Jika Anda menggunakan salah satu dari API streaming untuk pe-render-an di server (atau framework yang bergantung pada mereka), React juga akan menggunakan <Suspense>
untuk menangani kesalahan pada server. Jika sebuah komponen menimbulkan kesalahan pada server, React tidak akan membatalkan pe-renderan pada server. Sebagai gantinya, React akan mencari komponen <Suspense>
terdekat di atasnya dan menyertakan fallback-nya (seperti spinner) ke dalam HTML yang dihasilkan server. Pengguna akan tetap melihat spinner pada awalnya.
Pada klien, React akan mencoba me-render komponen yang sama kembali. Jika terjadi kesalahan pada klien juga, React akan melemparkan kesalahan dan menampilkan batasan error terdekat. Namun, jika tidak terjadi kesalahan pada klien, React tidak akan menampilkan kesalahan pada pengguna karena konten pada akhirnya berhasil ditampilkan.
Anda dapat menggunakan ini untuk mengecualikan beberapa komponen dari perenderan di server. Untuk melakukan hal ini, lemparkan kesalahan pada lingkungan server dan kemudian bungkus dengan batas <Suspense>
untuk mengganti HTML-nya dengan fallback:
<Suspense fallback={<Loading />}>
<Chat />
</Suspense>
function Chat() {
if (typeof window === 'undefined') {
throw Error('Chat seharusnya hanya di-render di klien.');
}
// ...
}
HTML server akan menyertakan indikator pemuatan. Indikator ini akan digantikan oleh komponen Chat
pada klien.
Pemecahan Masalah
Bagaimana cara mencegah agar UI tidak diganti dengan fallback selama pembaruan?
Mengganti UI yang terlihat dengan fallback menciptakan pengalaman pengguna yang mengejutkan. Hal ini dapat terjadi ketika pembaruan menyebabkan sebuah komponen menjadi ditangguhkan, dan batasan Suspense terdekat sudah menampilkan konten kepada pengguna.
Untuk mencegah hal ini terjadi, tandai pembaruan sebagai tidak mendesak dengan menggunakan startTransition
. Selama transisi, React akan menunggu hingga cukup banyak data yang dimuat untuk mencegah terjadinya kemumculan fallback yang tidak diinginkan:
function handleNextPageClick() {
// Jika pembaruan ini ditangguhkan, jangan sembunyikan konten yang sudah ditampilkan
startTransition(() => {
setCurrentPage(currentPage + 1);
});
}
Hal ini akan menghindari penyembunyan konten yang ada. Namun, setiap batasan Suspense
yang baru di-render masih akan segera menampilkan fallback untuk menghindari pemblokiran UI dan memperbolehkan pengguna melihat konten saat konten tersebut tersedia.
React hanya akan mencegah fallback yang tidak diinginkan selama pembaruan yang tidak mendesak. Ini tidak akan menunda pe-render-an jika hasl tersebut adalah hasil dari pembaruan yang mendesak. Anda harus memilih menggunakan API seperti startTransition
atau useDeferredValue
.
Jika router Anda terintegrasi dengan Suspense, router seharusnya membungkus pembaruannya menjadi startTransition
secara otomatis.