Suspense pokazuje widok zastępczy (fallback), na przykład komunikat ładowania, dopóki komponenty w jego środku nie są jeszcze gotowe. Najczęściej czeka się na React.lazy: ładuje kod komponentu dopiero wtedy, gdy ten komponent pierwszy raz się renderuje, dzięki czemu początkowe pobieranie jest małe.
Kliknij Show chart: na sekundę pojawia się Loading chart..., a potem wykres. Ukryj go i pokaż ponownie, a pojawi się od razu, bez nowej linii logu, bo lazy zachowuje załadowany moduł.
Podział kodu z lazy
Edytor trzyma wszystko w jednym pliku, więc przykład buduje wolny moduł ręcznie: obietnicę, która po sekundzie rozwiązuje się do obiektu z eksportem default. W prawdziwej aplikacji komponent żyje we własnym pliku, a ty przekazujesz dynamiczny import:
import { lazy, Suspense } from 'react';
const Chart = lazy(() => import('./Chart.jsx'));
export default function Dashboard() {
return (
<Suspense fallback={<p>Loading chart...</p>}>
<Chart />
</Suspense>
);
}
import('./Chart.jsx') zwraca obietnicę modułu. Bundlery takie jak Vite i webpack widzą dynamiczny import i umieszczają Chart.jsx oraz wszystko, czego używa tylko on, w osobnym pliku, pobieranym przy pierwszym renderowaniu <Chart />. Zasady, które warto znać:
- Moduł potrzebuje eksportu domyślnego.
lazyodczytuje właściwośćdefaultz tego, do czego rozwiązuje się obietnica. Dla eksportu nazwanego zmapuj go:lazy(() => import('./charts.js').then((m) => ({ default: m.LineChart }))). - Wywołuj
lazyna najwyższym poziomie modułu. Wewnątrz komponentu tworzyłoby nowy typ komponentu przy każdym renderowaniu, więc React odmontowywałby stary, tracił jego stan i ładował go ponownie. - Dziel tam, gdzie użytkownik i tak czeka. Dobrymi kandydatami są trasy, okna modalne, rzadko otwierane panele i ciężkie widżety (edytory, wykresy, mapy). Dzielenie każdego małego komponentu dodaje żądania i stany ładowania bez żadnej korzyści.
Wstępne ładowanie przed kliknięciem
Leniwy komponent zaczyna się ładować przy pierwszym renderowaniu, więc użytkownik po kliknięciu zawsze czeka co najmniej na jedno pobieranie. Jeśli możesz przewidzieć, że kliknięcie nadejdzie, zacznij pobieranie wcześniej. Trzymaj funkcję importu w zmiennej i wywołaj ją przy najechaniu albo fokusie; przeglądarka zachowuje moduł, więc gdy lazy później wywoła ten sam import, rozwiąże się bez drugiego pobierania.
const loadChart = () => import('./Chart.jsx');
const Chart = lazy(loadChart);
<button onMouseEnter={loadChart} onFocus={loadChart} onClick={() => setShow(true)}>
Show chart
</button>
Jak Suspense decyduje, co pokazać
Gdy komponent wewnątrz <Suspense> nie jest gotowy, zawiesza się: React przestaje renderować tę część i pokazuje fallback najbliższego Suspense nad nim. Cała zawartość tej granicy zostaje zastąpiona widokiem zastępczym, a nie tylko komponent, który czeka. Gdy to, na co czekał, jest gotowe, React ponownie renderuje treść i zastępuje widok zastępczy.
Dlatego położenie granicy to decyzja projektowa. Niezależne części umieszczaj we własnych granicach, żeby każda mogła się pojawić, gdy będzie gotowa:
Najpierw cała strona pokazuje Loading page..., bo Header należy do zewnętrznej granicy. Gdy nagłówek jest gotowy, pojawia się artykuł z Loading comments... pod spodem, a komentarze przychodzą na końcu. Zmień 1500 na 3000, a podgląd przeładuje się z dłuższym czekaniem tylko na komentarze. Usuń wewnętrzne <Suspense> (zostaw <Comments />), a strona przed pokazaniem czegokolwiek będzie czekać na komentarze.
Suspense z use() w React 19
W React 19 komponent może odczytać obietnicę przez use(promise). Jeśli obietnica wciąż oczekuje, komponent się zawiesza, a najbliższe Suspense pokazuje swój widok zastępczy; gdy się rozwiąże, use zwraca wartość. Strona o hooku use omawia to w całości. Udawane fetchUser poniżej zastępuje prawdziwe żądanie.
Przejdź po kolei przez użytkowników, a potem wróć do User 1: pojawia się natychmiast, a konsola nie wypisuje nowego pobierania, bo obietnica jest już w cache.
Dlaczego obietnica musi być zapamiętana
Mapa cache nie jest tu optymalizacją, jest wymagana. Komponent, który się zawiesza, nie zachowuje niczego z tej próby: gdy obietnica się rozwiąże, React renderuje go ponownie od początku. Gdyby Profile wywoływał fetchUser(id) bezpośrednio, każda próba tworzyłaby nową obietnicę, rozpoczynała nowe żądanie i znowu się na nim zawieszała. Profil nigdy by się nie pojawił, a konsola zapełniłaby się liniami fetching user. Obietnica musi więc pochodzić z miejsca, które przetrwa renderowanie:
- z cache z kluczem żądania, jak mapa powyżej (biblioteki danych takie jak TanStack Query i loadery frameworków robią to za ciebie);
- od rodzica, który tworzy obietnicę raz, w handlerze zdarzenia albo w komponencie serwerowym (Server Component), i przekazuje ją w dół jako prop.
Zwróć uwagę, że każdy nowy użytkownik nadal zastępuje profil widokiem zastępczym. Jeśli wolisz zostawić starego użytkownika na ekranie, dopóki następny nie będzie gotowy, owiń aktualizację w przejście: startTransition(() => setId(n)). Przy przejściu React nie ukrywa treści, która jest już widoczna (zobacz useTransition).
Błędy potrzebują granicy błędów
Suspense obsługuje czekanie, a nie niepowodzenia. Jeśli leniwy import się nie uda (użytkownik stracił połączenie, nowe wdrożenie usunęło stary kawałek kodu) albo obietnica przekazana do use zostanie odrzucona, React rzuca błąd do najbliższej granicy błędów. Bez niej całe drzewo pod rootem zostaje odmontowane. Granice błędów to wciąż komponenty klasowe (zobacz granice błędów):
<ErrorBoundary fallback={<p>Could not load the chart.</p>}>
<Suspense fallback={<p>Loading chart...</p>}>
<Chart />
</Suspense>
</ErrorBoundary>
Czego Suspense nie wykrywa
Suspense reaguje tylko na komponenty, które się zawieszają: komponenty lazy, use(promise) i źródła danych zbudowane dla Suspense (loadery frameworków, biblioteki obsługujące Suspense). Fetch wewnątrz useEffect, który po zakończeniu ustawia stan, nie zawiesza komponentu, więc granica Suspense wokół niego nigdy nie pokazuje widoku zastępczego. Przy takim wzorcu trzymasz własny stan loading, jak pokazuje strona o pobieraniu danych.
Najczęściej zadawane pytania
Co to jest React Suspense?
<Suspense fallback={...}> to komponent, który pokazuje swój fallback, dopóki którykolwiek komponent w jego środku na coś czeka, na przykład na leniwie ładowany kod albo dane odczytywane przez use. Gdy wszystko w środku jest gotowe, React zamienia widok zastępczy na treść.
Co robi React.lazy?
lazy(() => import('./Chart.jsx')) tworzy komponent, którego kod jest pobierany przy pierwszym renderowaniu. Bundlery umieszczają ten plik w osobnym kawałku (chunk), więc strona na start ładuje mniej JavaScriptu.
Czy Suspense działa przy pobieraniu danych?
Tak, gdy źródło danych to obsługuje. W React 19 komponent może odczytać obietnicę przez use(promise) i zawiesić się, dopóki się nie rozwiąże. Frameworki takie jak Next.js też integrują Suspense ze swoim ładowaniem danych. Fetch wewnątrz useEffect nie uruchamia Suspense.
Jak obsługiwać błędy z Suspense?
Suspense obsługuje tylko czekanie. Jeśli leniwy import albo obietnica się nie powiedzie, błąd trafia do najbliższej granicy błędów (error boundary), więc owiń w nią granicę Suspense (albo jej rodzica).
Gdzie wywoływać lazy?
Na najwyższym poziomie modułu, poza jakimkolwiek komponentem. Wywołanie lazy wewnątrz komponentu tworzy nowy typ komponentu przy każdym renderowaniu, co resetuje jego stan i ładuje go ponownie.