ข้ามไปยังเนื้อหา

Suspense & lazy

การจัดการ loading state เองหมายถึงต้องร้อย isLoading boolean ผ่าน component และ render spinner เป็นสิบ ๆ จุด Suspense แทนที่ทั้งหมดนั้นด้วย boundary: คุณครอบบางส่วนของ tree ใส่ fallback ให้ และระหว่างที่มีอะไรข้างในยัง “ไม่พร้อม” React แสดง fallback แทน

import { Suspense } from "react";
function Page() {
return (
<Suspense fallback={<Spinner />}>
<Profile /> {/* ถ้าอันนี้ suspend, Spinner จะโชว์ */}
<Timeline /> {/* จนกว่าทั้งคู่พร้อม แล้วทั้งคู่จะโผล่พร้อมกัน */}
</Suspense>
);
}

คุณไม่ต้องเขียน “if loading show spinner” ที่ไหนเลย boundary เป็นเจ้าของ loading UI ของ subtree ตัวเอง แบบ declarative

component จะ suspend เมื่อ render ไม่จบเพราะกำลังรออะไรบางอย่าง — code ที่ยังไม่ถูกโหลด (React.lazy) หรือ data (ผ่าน use() hook หรือ data library ที่รองรับ Suspense) เมื่อ component suspend React จะ “ย้อนขึ้น” ไปหา <Suspense> ที่ใกล้ที่สุดด้านบนและแสดง fallback ของ boundary นั้น เมื่อสิ่งที่รอ resolve แล้ว React จะลอง render ใหม่และสลับ content จริงเข้ามา

flowchart TB
  boundary["Suspense fallback=Spinner"] --> child["Child suspend
(รอ code หรือ data)"]
  child --> show["React แสดง Spinner"]
  show --> resolve["สิ่งที่รอ resolve แล้ว"]
  resolve --> retry["React render ใหม่
→ แสดง content จริง"]
child ที่ suspend แสดง fallback ของ boundary ที่ใกล้ที่สุด

การใช้ Suspense ที่พบบ่อยที่สุดคือ lazy-loading component เพื่อไม่ให้ code ของตัวเองอยู่ใน initial bundle React.lazy รับ dynamic import() และคืน component ที่ suspend จนกว่า chunk จะโหลดเสร็จ

import { lazy, Suspense } from "react";
// code ของ SettingsPanel เป็น chunk แยก, fetch ตอน render ครั้งแรกเท่านั้น
const SettingsPanel = lazy(() => import("./SettingsPanel"));
function App({ showSettings }) {
return (
<Suspense fallback={<Spinner />}>
{showSettings && <SettingsPanel />}
</Suspense>
);
}

จนกว่า SettingsPanel จะถูก render จริง JavaScript ของตัวเองจะไม่ถูกโหลดเลย — win ตรง ๆ กับเวลา initial load พอ render ครั้งแรก จะ suspend (กำลังโหลด), fallback โชว์, แล้ว panel ค่อยโผล่

Suspense boundary nest กันได้ และคุณวางตำแหน่ง boundary เพื่อควบคุม granularity ของการโหลด boundary ที่อยู่สูงใน tree หมายถึง “แสดง spinner ใหญ่อันเดียวสำหรับทุกอย่างข้างล่าง” boundary ที่ลึกลงไปหมายถึง “เผย shell ทันที และแสดง spinner เล็ก ๆ เฉพาะส่วนที่ยังโหลดอยู่”

<Suspense fallback={<PageSkeleton />}>
<Header />
<Suspense fallback={<FeedSkeleton />}>
<Feed /> {/* header โชว์ระหว่างที่มีแค่ feed กำลังโหลด */}
</Suspense>
</Suspense>

การเลือกว่าจะวาง boundary ตรงไหนเป็นการตัดสินใจด้าน UX: boundary น้อย = ง่ายกว่าแต่ “all or nothing” มากกว่า; boundary เยอะ = เผยทีละส่วน

boundary `<Suspense fallback={...}>` ทำอะไร?
component "suspend" หมายความว่าอะไร?
`React.lazy(() => import("./X"))` ทำอะไร?
ทำไมต้อง nest Suspense หลาย boundary?