Suspense
In Lazy Loading, we only saw Suspense paired with lazy(). This
lesson takes a closer look at Suspense itself -- what it does, how it
can be nested, and what it does NOT do automatically.
The fallback Prop
Suspense shows a fallback while something INSIDE it isn't ready
yet:
import { lazy, Suspense } from "react";
const CourseDetails = lazy(() => import("./CourseDetails.jsx"));
function SuspenseFallbackExample() {
return (
// `fallback`, herhangi bir JSX olabilir -- yalnızca bir metin değil,
// bir spinner, bir iskelet (skeleton) ekran, ya da başka bir
// component de olabilir. Lazy Loading dersinde Suspense'i YALNIZCA
// lazy() ile birlikte gördük -- bu ders, Suspense'in kendisine daha
// yakından bakıyor.
<Suspense fallback={<p className="spinner">Loading course...</p>}>
<CourseDetails />
</Suspense>
);
}
fallback can be ANY JSX, not just text -- a spinner, a skeleton
screen, or another component. Once the component inside (here,
CourseDetails) is ready, fallback is automatically REPLACED with
the real content.
Nested Suspense Boundaries
Multiple Suspense boundaries can be nested at different levels:
import { lazy, Suspense } from "react";
const CourseHeader = lazy(() => import("./CourseHeader.jsx"));
const CourseReviews = lazy(() => import("./CourseReviews.jsx"));
function NestedSuspenseExample() {
return (
// Dıştaki Suspense, CourseHeader yüklenene kadar TÜM sayfa için bir
// fallback gösterir. CourseHeader göründükten sonra, İÇTEKİ Suspense
// yalnızca CourseReviews'un yerini kaplar -- sayfanın geri kalanı
// (CourseHeader dahil) tekrar "loading" durumuna DÖNMEZ.
<Suspense fallback={<p>Loading page...</p>}>
<CourseHeader />
<Suspense fallback={<p>Loading reviews...</p>}>
<CourseReviews />
</Suspense>
</Suspense>
);
}
The outer Suspense shows a fallback for the WHOLE page until
CourseHeader loads. Once CourseHeader appears, the INNER Suspense
only covers CourseReviews -- the rest of the page does NOT go back to
a "loading" state. This gives users a smoother experience: instead of
everything disappearing and reappearing at once, only the part that's
still waiting shows "loading."
Suspense with the use() Hook
The use() hook in React 19 can integrate a Promise DIRECTLY with
Suspense:
import { Suspense, use } from "react";
function fetchCourse() {
return fetch("http://localhost:3000/courses/1").then((response) => response.json());
}
// Her render'da YENİ bir Promise oluşturmamak için, bunu component'in
// DIŞINDA, modül yüklenirken bir kez çağırıyoruz.
const coursePromise = fetchCourse();
function CourseName() {
// use(), normal hook'ların aksine KOŞULLU olarak da çağrılabilir. Bir
// Promise verildiğinde, use() Promise HENÜZ çözülmediyse React'e
// "beklemem gerekiyor" der -- bu, en yakın Suspense'in fallback'ini
// gösterir; Promise çözülünce gerçek değeri döner.
const course = use(coursePromise);
return <p>Course: {course.name}</p>;
}
function UsePromiseWithSuspenseExample() {
return (
<Suspense fallback={<p>Loading course...</p>}>
<CourseName />
</Suspense>
);
}
Unlike other hooks, use() can also be called CONDITIONALLY. Given a
Promise, if it hasn't RESOLVED yet, it tells React "I need to wait" --
this shows the nearest Suspense's fallback; once the Promise
resolves, use() returns the actual value and the component renders
normally.
What Suspense Doesn't Do Automatically
An important gotcha: not every asynchronous operation triggers Suspense automatically:
import { Suspense, useEffect, useState } from "react";
function CourseListWithEffect() {
const [courses, setCourses] = useState(null);
// ÖNEMLİ: useEffect + fetch (API & Data Fetching dersindeki desen),
// Suspense'i OTOMATİK OLARAK TETİKLEMEZ -- Suspense yalnızca use() gibi,
// React'in DOĞRUDAN tanıdığı bir Promise kaynağıyla çalışır. Bu yüzden
// burada `courses` state'i null'ken, dışarıdaki Suspense'in fallback'i
// GÖRÜNMEZ -- component `null` render eder, Suspense'in haberi bile
// olmaz.
useEffect(() => {
fetch("http://localhost:3000/courses")
.then((response) => response.json())
.then((data) => setCourses(data));
}, []);
if (!courses) {
// Suspense'in fallback'i değil, kendi manuel loading kontrolümüz.
return null;
}
return (
<ul>
{courses.map((course) => (
<li key={course.id}>{course.name}</li>
))}
</ul>
);
}
function SuspenseLimitationsExample() {
return (
<Suspense fallback={<p>This fallback never shows for CourseListWithEffect.</p>}>
<CourseListWithEffect />
</Suspense>
);
}
The useEffect + fetch pattern from API & Data Fetching does NOT
automatically trigger Suspense -- Suspense only works with a Promise
source that React DIRECTLY recognizes, like use(). A component using
fetch inside useEffect still needs to manage its own loading
state ITSELF.
Summary and Glossary
Suspense shows a fallback while something inside it isn't ready
yet; once the content is ready, it's automatically replaced. Multiple
Suspense boundaries can be nested to show loading states at
different granularities. The use() hook in React 19 integrates a
Promise with Suspense. But "classic" data-fetching patterns like
useEffect + fetch do NOT automatically trigger Suspense -- only
sources React directly supports, like use(), do.
Glossary
Suspense — A React component that shows a fallback UI while a resource inside it (a lazy component, a Promise) isn't ready yet.
Suspense Boundary — The area covered by a <Suspense> component,
with its own fallback.