Suspense

Suspense's fallback prop, nested Suspense boundaries, integrating Suspense with React 19's use() hook, and what Suspense doesn't do automatically -- with simple examples.

Advanced 11 min
TR

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.