<Suspense> memungkinkan Anda menampilkan fallback hingga komponen anak-anaknya selesai dimuat.

<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>

Referensi

<Suspense>

Props

  • children: UI sebenarnya yang ingin Anda render. Jika children ditangguhkan saat di-render, maka Suspense akan beralih me-render fallback.
  • 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 ke fallback ketika children ditangguhkan, dan kembali ke children ketika datanya sudah siap. Jika fallback 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 oleh startTransition atau useDeferredValue.
  • 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>;
}

Catatan

Hanya sumber data yang mendukung Suspense yang akan mengaktifkan komponen Suspense. Yaitu:

  • Pengambilan data dengan framework yang mendukung Suspense seperti Relay dan Next.js
  • Kode komponen lazy-loading dengan lazy
  • Membaca nilai dari Promise dengan use

Suspense tidak mendeteksi ketika data diambil di dalam Effect atau event handler.

Cara yang tepat untuk memuat data dalam komponen Albums di atas tergantung pada framework Anda. Jika Anda menggunakan framework yang mendukung Suspense, Anda akan menemukan detailnya dalam dokumentasi pengambilan data.

Pengambilan data yang mendukung Suspense tanpa menggunakan framework dogmatis belum didukung. Persyaratan untuk mengimplementasikan sumber data yang mendukung Suspense masih belum stabil dan belum terdokumentasi. API resmi untuk mengintegrasikan sumber data dengan Suspense akan dirilis pada versi React yang akan datang.


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:

  1. Jika Biography belum dimuat, BigSpinner ditampilkan sebagai pengganti seluruh area konten.
  2. Setelah Biography selesai dimuat, BigSpinner digantikan oleh konten.
  3. Jika Albums belum dimuat, AlbumsGlimmer ditampilkan sebagai pengganti Albums dan induknya Panel.
  4. Akhirnya, setelah Albums selesai dimuat, dia akan menggantikan 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>🌀 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>
    </>
  );
}

Catatan

Baik nilai yang ditangguhkan maupun transisi memungkinkan Anda menghindari menampilkan fallback Suspense hanya untuk indikator sebaris. Transisi menandai seluruh pembaruan sebagai tidak mendesak sehingga biasanya hal ini digunakan oleh framework dan library router untuk navigasi. Nilai yang ditangguhkan, di sisi lain, sebagian besar berguna dalam kode aplikasi di mana Anda ingin menandai bagian dari UI sebagai tidak mendesak dan membiarkannya “tertinggal” dari UI lainnya.


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.

Catatan

Router yang mendukung Suspense diharapkan untuk membungkus pembaruan navigasi ke dalam transisi secara bawaan.


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.