ไทย

ฝึกฝน React Suspense สำหรับการดึงข้อมูล เรียนรู้การจัดการ loading state แบบ declarative, ปรับปรุง UX ด้วย transitions, และรับมือข้อผิดพลาดด้วย Error Boundaries

React Suspense Boundaries: เจาะลึกการจัดการ Loading State แบบ Declarative

ในโลกของการพัฒนาเว็บสมัยใหม่ การสร้างประสบการณ์ผู้ใช้ที่ราบรื่นและตอบสนองได้ดีเป็นสิ่งสำคัญสูงสุด หนึ่งในความท้าทายที่นักพัฒนาต้องเผชิญอยู่เสมอคือการจัดการสถานะการโหลด (loading states) ตั้งแต่การดึงข้อมูลโปรไฟล์ผู้ใช้ไปจนถึงการโหลดส่วนใหม่ของแอปพลิเคชัน ช่วงเวลาแห่งการรอคอยเหล่านี้มีความสำคัญอย่างยิ่ง ในอดีต สิ่งนี้เกี่ยวข้องกับแฟล็กบูลีนที่ยุ่งเหยิงเช่น isLoading, isFetching, และ hasError ซึ่งกระจายอยู่ทั่วคอมโพเนนต์ของเรา แนวทางแบบ imperative นี้ทำให้โค้ดของเราเกะกะ ตรรกะซับซ้อน และมักเป็นบ่อเกิดของบั๊ก เช่น race conditions

ขอแนะนำ React Suspense ในตอนแรกถูกนำมาใช้สำหรับการทำ code-splitting ด้วย React.lazy() แต่ความสามารถของมันได้ขยายออกไปอย่างมากใน React 18 จนกลายเป็นกลไกหลักอันทรงพลังสำหรับการจัดการกับการทำงานแบบอะซิงโครนัส โดยเฉพาะการดึงข้อมูล Suspense ช่วยให้เราสามารถจัดการ loading states ในรูปแบบ declarative ซึ่งเปลี่ยนวิธีการเขียนและให้เหตุผลเกี่ยวกับคอมโพเนนต์ของเราไปโดยสิ้นเชิง แทนที่จะถามว่า "ฉันกำลังโหลดอยู่หรือเปล่า?" คอมโพเนนต์ของเราสามารถพูดง่ายๆ ว่า "ฉันต้องการข้อมูลนี้เพื่อ render ขณะที่ฉันรอ โปรดแสดง UI สำรองนี้"

คู่มือฉบับสมบูรณ์นี้จะนำคุณเดินทางจากวิธีการจัดการ state แบบดั้งเดิมไปสู่กระบวนทัศน์แบบ declarative ของ React Suspense เราจะสำรวจว่า Suspense boundaries คืออะไร ทำงานอย่างไรทั้งกับการทำ code-splitting และการดึงข้อมูล และวิธีจัดการ UI การโหลดที่ซับซ้อนซึ่งจะทำให้ผู้ใช้ของคุณพึงพอใจแทนที่จะรู้สึกหงุดหงิด

วิธีเก่า: ความยุ่งยากของการจัดการ Loading States ด้วยตนเอง

ก่อนที่เราจะเข้าใจความสง่างามของ Suspense ได้อย่างเต็มที่ สิ่งสำคัญคือต้องเข้าใจปัญหาก่อนว่ามันช่วยแก้ปัญหาอะไร ลองดูคอมโพเนนต์ทั่วไปที่ดึงข้อมูลโดยใช้ useEffect และ useState hooks

ลองจินตนาการถึงคอมโพเนนต์ที่ต้องการดึงและแสดงข้อมูลผู้ใช้:


import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // รีเซ็ต state สำหรับ userId ใหม่
    setIsLoading(true);
    setUser(null);
    setError(null);

    const fetchUser = async () => {
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err);
      } finally {
        setIsLoading(false);
      }
    };

    fetchUser();
  }, [userId]); // ดึงข้อมูลใหม่เมื่อ userId เปลี่ยนแปลง

  if (isLoading) {
    return <p>Loading profile...</p>;
  }

  if (error) {
    return <p>Error: {error.message}</p>;
  }

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

รูปแบบนี้ใช้งานได้ แต่มีข้อเสียหลายประการ:

ขอแนะนำ React Suspense: การเปลี่ยนแปลงกระบวนทัศน์

Suspense พลิกโมเดลนี้กลับหัวกลับหาง แทนที่คอมโพเนนต์จะจัดการสถานะการโหลดภายใน มันจะสื่อสารการพึ่งพาการทำงานแบบอะซิงโครนัสไปยัง React โดยตรง หากข้อมูลที่ต้องการยังไม่พร้อมใช้งาน คอมโพเนนต์จะ "ระงับ" (suspend) การเรนเดอร์

เมื่อคอมโพเนนต์ระงับการทำงาน React จะเดินขึ้นไปตาม component tree เพื่อค้นหา Suspense Boundary ที่ใกล้ที่สุด Suspense Boundary คือคอมโพเนนต์ที่คุณกำหนดใน tree ของคุณโดยใช้ <Suspense> จากนั้น boundary นี้จะเรนเดอร์ UI สำรอง (fallback UI) (เช่น spinner หรือ skeleton loader) จนกว่าคอมโพเนนต์ทั้งหมดที่อยู่ภายในจะแก้ไขการพึ่งพาข้อมูลของตนได้สำเร็จ

แนวคิดหลักคือการจัดวางการพึ่งพาข้อมูลไว้ที่เดียวกับคอมโพเนนต์ที่ต้องการมัน ในขณะที่รวมศูนย์ UI การโหลดไว้ในระดับที่สูงขึ้นใน component tree สิ่งนี้ช่วยให้ตรรกะของคอมโพเนนต์สะอาดขึ้น และให้คุณควบคุมประสบการณ์การโหลดของผู้ใช้ได้อย่างมีประสิทธิภาพ

คอมโพเนนต์ "ระงับ" การทำงานได้อย่างไร?

ความมหัศจรรย์เบื้องหลัง Suspense อยู่ในรูปแบบที่อาจดูแปลกในตอนแรก: การ throw a Promise แหล่งข้อมูลที่เปิดใช้งาน Suspense จะทำงานดังนี้:

  1. เมื่อคอมโพเนนต์ขอข้อมูล แหล่งข้อมูลจะตรวจสอบว่ามีข้อมูลนั้นอยู่ในแคชหรือไม่
  2. หากข้อมูลพร้อมใช้งาน มันจะส่งคืนข้อมูลนั้นแบบซิงโครนัส
  3. หากข้อมูลยังไม่พร้อมใช้งาน (เช่น กำลังถูกดึงข้อมูลอยู่) แหล่งข้อมูลจะ throws the Promise ที่แสดงถึงคำขอดึงข้อมูลที่กำลังดำเนินอยู่

React จะจับ Promise ที่ถูก throw นี้ มันไม่ทำให้แอปของคุณพัง แต่จะตีความว่าเป็นสัญญาณว่า: "คอมโพเนนต์นี้ยังไม่พร้อมที่จะเรนเดอร์ หยุดการทำงานชั่วคราว และมองหา Suspense boundary ด้านบนเพื่อแสดง fallback" เมื่อ Promise ได้รับการ resolve แล้ว React จะพยายามเรนเดอร์คอมโพเนนต์อีกครั้ง ซึ่งตอนนี้จะได้รับข้อมูลและเรนเดอร์ได้สำเร็จ

The <Suspense> Boundary: ตัวประกาศ UI สำหรับการโหลดของคุณ

คอมโพเนนต์ <Suspense> คือหัวใจของรูปแบบนี้ ใช้งานง่ายอย่างไม่น่าเชื่อ โดยรับ prop ที่จำเป็นเพียงตัวเดียวคือ fallback


import { Suspense } from 'react';

function App() {
  return (
    <div>
      <h1>My Application</h1>
      <Suspense fallback={<p>Loading content...</p>}>
        <SomeComponentThatFetchesData />
      </Suspense>
    </div>
  );
}

ในตัวอย่างนี้ หาก SomeComponentThatFetchesData ระงับการทำงาน ผู้ใช้จะเห็นข้อความ "Loading content..." จนกว่าข้อมูลจะพร้อมใช้งาน fallback สามารถเป็น React node ที่ถูกต้องได้ทุกชนิด ตั้งแต่สตริงธรรมดาไปจนถึงคอมโพเนนต์ skeleton ที่ซับซ้อน

กรณีการใช้งานคลาสสิก: Code Splitting ด้วย React.lazy()

การใช้งาน Suspense ที่เป็นที่ยอมรับมากที่สุดคือการทำ code splitting ซึ่งช่วยให้คุณเลื่อนการโหลด JavaScript ของคอมโพเนนต์ออกไปจนกว่าจะจำเป็นต้องใช้งานจริง


import React, { Suspense, lazy } from 'react';

// โค้ดของคอมโพเนนต์นี้จะไม่อยู่ใน bundle เริ่มต้น
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <div>
      <h2>Some content that loads immediately</h2>
      <Suspense fallback={<div>Loading component...</div>}>
        <HeavyComponent />
      </Suspense>
    </div>
  );
}

ในที่นี้ React จะดึง JavaScript สำหรับ HeavyComponent ก็ต่อเมื่อพยายามเรนเดอร์เป็นครั้งแรกเท่านั้น ในขณะที่กำลังดึงและแยกวิเคราะห์ Suspense fallback จะถูกแสดง นี่เป็นเทคนิคที่มีประสิทธิภาพในการปรับปรุงเวลาในการโหลดหน้าเว็บครั้งแรก

ขอบเขตสมัยใหม่: การดึงข้อมูลด้วย Suspense

ในขณะที่ React มีกลไก Suspense ให้ แต่ไม่ได้มี data-fetching client ที่เฉพาะเจาะจงมาให้ หากต้องการใช้ Suspense สำหรับการดึงข้อมูล คุณต้องมีแหล่งข้อมูลที่ทำงานร่วมกับมันได้ (เช่น แหล่งข้อมูลที่ throw a Promise เมื่อข้อมูลยังไม่พร้อม)

เฟรมเวิร์กอย่าง Relay และ Next.js มีการรองรับ Suspense เป็นฟีเจอร์หลักในตัว ไลบรารีดึงข้อมูลยอดนิยมอย่าง TanStack Query (เดิมคือ React Query) และ SWR ก็มีการรองรับแบบทดลองหรือแบบเต็มรูปแบบเช่นกัน

เพื่อทำความเข้าใจแนวคิด เรามาสร้าง wrapper แบบง่ายๆ รอบ fetch API เพื่อให้เข้ากันได้กับ Suspense กัน หมายเหตุ: นี่เป็นตัวอย่างที่เรียบง่ายเพื่อการศึกษาและไม่พร้อมสำหรับการใช้งานจริง ขาดการจัดการแคชและข้อผิดพลาดที่เหมาะสม


// data-fetcher.js
// แคชอย่างง่ายสำหรับเก็บผลลัพธ์
const cache = new Map();

export function fetchData(url) {
  if (!cache.has(url)) {
    cache.set(url, { status: 'pending', promise: fetchAndCache(url) });
  }

  const record = cache.get(url);

  if (record.status === 'pending') {
    throw record.promise; // นี่คือความมหัศจรรย์!
  }
  if (record.status === 'error') {
    throw record.error;
  }
  if (record.status === 'success') {
    return record.data;
  }
}

async function fetchAndCache(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Fetch failed with status ${response.status}`);
    }
    const data = await response.json();
    cache.set(url, { status: 'success', data });
  } catch (e) {
    cache.set(url, { status: 'error', error: e });
  }
}

wrapper นี้จะรักษาสถานะอย่างง่ายสำหรับแต่ละ URL เมื่อ fetchData ถูกเรียก มันจะตรวจสอบสถานะ หากเป็น pending มันจะ throw promise หากสำเร็จ มันจะส่งคืนข้อมูล ตอนนี้เรามาเขียนคอมโพเนนต์ UserProfile ของเราใหม่โดยใช้สิ่งนี้


// UserProfile.js
import React, { Suspense } from 'react';
import { fetchData } from './data-fetcher';

// คอมโพเนนต์ที่ใช้ข้อมูลจริงๆ
function ProfileDetails({ userId }) {
  // พยายามอ่านข้อมูล หากยังไม่พร้อม ส่วนนี้จะ suspend
  const user = fetchData(`https://api.example.com/users/${userId}`);

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

// คอมโพเนนท์แม่ที่กำหนด UI ของสถานะการโหลด
export function UserProfile({ userId }) {
  return (
    <Suspense fallback={<p>Loading profile...</p>}>
      <ProfileDetails userId={userId} />
    </Suspense>
  );
}

ดูความแตกต่างสิ! คอมโพเนนต์ ProfileDetails นั้นสะอาดและมุ่งเน้นไปที่การเรนเดอร์ข้อมูลเพียงอย่างเดียว ไม่มีสถานะ isLoading หรือ error เลย มันแค่ร้องขอข้อมูลที่ต้องการ ความรับผิดชอบในการแสดงตัวบ่งชี้การโหลดได้ถูกย้ายขึ้นไปยังคอมโพเนนต์แม่ UserProfile ซึ่งระบุอย่างชัดเจนว่าจะแสดงอะไรในขณะที่รอ

การจัดการสถานะการโหลดที่ซับซ้อน

พลังที่แท้จริงของ Suspense จะปรากฏชัดเมื่อคุณสร้าง UI ที่ซับซ้อนซึ่งมีการพึ่งพาแบบอะซิงโครนัสหลายอย่าง

Nested Suspense Boundaries สำหรับ UI ที่แสดงผลแบบเหลื่อมเวลา

คุณสามารถซ้อน Suspense boundaries เพื่อสร้างประสบการณ์การโหลดที่ละเอียดยิ่งขึ้น ลองจินตนาการถึงหน้าแดชบอร์ดที่มีแถบด้านข้าง (sidebar), พื้นที่เนื้อหาหลัก (main content area), และรายการกิจกรรมล่าสุด (list of recent activities) ซึ่งแต่ละส่วนอาจต้องการการดึงข้อมูลของตัวเอง


function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <div className="layout">
        <Suspense fallback={<p>Loading navigation...</p>}>
          <Sidebar />
        </Suspense>

        <main>
          <Suspense fallback={<ProfileSkeleton />}>
            <MainContent />
          </Suspense>

          <Suspense fallback={<ActivityFeedSkeleton />}>
            <ActivityFeed />
          </Suspense>
        </main>
      </div>
    </div>
  );
}

ด้วยโครงสร้างนี้:

สิ่งนี้ช่วยให้คุณสามารถแสดงเนื้อหาที่เป็นประโยชน์ต่อผู้ใช้ได้โดยเร็วที่สุด ซึ่งช่วยปรับปรุงประสิทธิภาพที่รับรู้ได้อย่างมาก

การหลีกเลี่ยง UI "Popcorning"

บางครั้ง แนวทางแบบเหลื่อมเวลานี้อาจทำให้เกิดเอฟเฟกต์ที่น่ารำคาญซึ่ง spinner หลายตัวปรากฏขึ้นและหายไปอย่างรวดเร็วติดต่อกัน ซึ่งมักเรียกว่า "popcorning" เพื่อแก้ปัญหานี้ คุณสามารถย้าย Suspense boundary ขึ้นไปสูงขึ้นใน tree ได้


function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<DashboardSkeleton />}>
        <div className="layout">
          <Sidebar />
          <main>
            <MainContent />
            <ActivityFeed />
          </main>
        </div>
      </Suspense>
    </div>
  );
}

ในเวอร์ชันนี้ DashboardSkeleton เพียงตัวเดียวจะแสดงขึ้นจนกว่าทั้งหมดของคอมโพเนนต์ลูก (Sidebar, MainContent, ActivityFeed) จะมีข้อมูลพร้อม แดชบอร์ดทั้งหมดจะปรากฏขึ้นพร้อมกัน การเลือกระหว่าง boundaries ที่ซ้อนกันกับ boundary ระดับสูงเพียงตัวเดียวเป็นการตัดสินใจด้านการออกแบบ UX ที่ Suspense ทำให้การนำไปใช้เป็นเรื่องง่าย

การจัดการข้อผิดพลาดด้วย Error Boundaries

Suspense จัดการกับสถานะ pending ของ promise แต่แล้วสถานะ rejected ล่ะ? หาก promise ที่ถูก throw โดยคอมโพเนนต์ถูก reject (เช่น ข้อผิดพลาดของเครือข่าย) มันจะถูกจัดการเหมือนกับข้อผิดพลาดในการเรนเดอร์อื่นๆ ใน React

วิธีแก้ปัญหาคือการใช้ Error Boundaries Error Boundary คือ class component ที่กำหนด lifecycle method พิเศษ componentDidCatch() หรือ static method getDerivedStateFromError() มันจะจับข้อผิดพลาด JavaScript ที่ใดก็ได้ใน component tree ของลูกๆ, บันทึกข้อผิดพลาดเหล่านั้น, และแสดง UI สำรอง

นี่คือคอมโพเนนต์ Error Boundary แบบง่ายๆ:


import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    // อัปเดต state เพื่อให้การเรนเดอร์ครั้งต่อไปจะแสดง UI สำรอง
    return { hasError: true, error: error };
  }

  componentDidCatch(error, errorInfo) {
    // คุณยังสามารถบันทึกข้อผิดพลาดไปยังบริการรายงานข้อผิดพลาดได้
    console.error("Caught an error:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // คุณสามารถเรนเดอร์ UI สำรองแบบกำหนดเองได้
      return <h1>Something went wrong. Please try again.</h1>;
    }

    return this.props.children; 
  }
}

จากนั้นคุณสามารถรวม Error Boundaries เข้ากับ Suspense เพื่อสร้างระบบที่แข็งแกร่งซึ่งจัดการได้ทั้งสามสถานะ: pending, success, และ error


import { Suspense } from 'react';
import ErrorBoundary from './ErrorBoundary';
import { UserProfile } from './UserProfile';

function App() {
  return (
    <div>
      <h2>User Information</h2>
      <ErrorBoundary>
        <Suspense fallback={<p>Loading...</p>}>
          <UserProfile userId={123} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

ด้วยรูปแบบนี้ หากการดึงข้อมูลภายใน UserProfile สำเร็จ โปรไฟล์จะถูกแสดง หากยังไม่พร้อม (pending) fallback ของ Suspense จะถูกแสดง หากล้มเหลว fallback ของ Error Boundary จะถูกแสดง ตรรกะนี้เป็นแบบ declarative, ประกอบกันได้, และง่ายต่อการให้เหตุผล

Transitions: กุญแจสำคัญสู่การอัปเดต UI ที่ไม่ปิดกั้น

ยังมีชิ้นส่วนสุดท้ายของจิ๊กซอว์ ลองพิจารณาการโต้ตอบของผู้ใช้ที่กระตุ้นให้เกิดการดึงข้อมูลใหม่ เช่น การคลิกปุ่ม "Next" เพื่อดูโปรไฟล์ผู้ใช้อื่น ด้วยการตั้งค่าข้างต้น ทันทีที่คลิกปุ่มและ userId prop เปลี่ยนแปลง คอมโพเนนต์ UserProfile จะ suspend อีกครั้ง ซึ่งหมายความว่าโปรไฟล์ที่มองเห็นอยู่จะหายไปและถูกแทนที่ด้วย loading fallback ซึ่งอาจให้ความรู้สึกกระตุกและรบกวน

นี่คือจุดที่ transitions เข้ามามีบทบาท Transitions เป็นฟีเจอร์ใหม่ใน React 18 ที่ให้คุณทำเครื่องหมายการอัปเดต state บางอย่างว่าไม่เร่งด่วน เมื่อการอัปเดต state ถูกห่อหุ้มด้วย transition React จะยังคงแสดง UI เก่า (เนื้อหาที่ไม่อัปเดต) ในขณะที่เตรียมเนื้อหาใหม่ในเบื้องหลัง มันจะ commit การอัปเดต UI ก็ต่อเมื่อเนื้อหาใหม่พร้อมที่จะแสดงผลแล้วเท่านั้น

API หลักสำหรับสิ่งนี้คือ useTransition hook


import React, { useState, useTransition, Suspense } from 'react';
import { UserProfile } from './UserProfile';

function ProfileSwitcher() {
  const [userId, setUserId] = useState(1);
  const [isPending, startTransition] = useTransition();

  const handleNextClick = () => {
    startTransition(() => {
      setUserId(id => id + 1);
    });
  };

  return (
    <div>
      <button onClick={handleNextClick} disabled={isPending}>
        Next User
      </button>

      {isPending && <span> Loading new profile...</span>}

      <ErrorBoundary>
        <Suspense fallback={<p>Loading initial profile...</p>}>
          <UserProfile userId={userId} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

นี่คือสิ่งที่เกิดขึ้นตอนนี้:

  1. โปรไฟล์เริ่มต้นสำหรับ userId: 1 โหลดขึ้นมา โดยแสดง fallback ของ Suspense
  2. ผู้ใช้คลิก "Next User"
  3. การเรียก setUserId ถูกห่อหุ้มด้วย startTransition
  4. React เริ่มเรนเดอร์ UserProfile ด้วย userId ใหม่คือ 2 ในหน่วยความจำ ซึ่งทำให้มัน suspend
  5. สิ่งสำคัญคือ แทนที่จะแสดง fallback ของ Suspense React จะยังคงแสดง UI เก่า (โปรไฟล์ของผู้ใช้ 1) ไว้บนหน้าจอ
  6. ค่าบูลีน isPending ที่ส่งคืนโดย useTransition จะกลายเป็น true ซึ่งช่วยให้เราสามารถแสดงตัวบ่งชี้การโหลดแบบ inline ที่ละเอียดอ่อนได้โดยไม่ต้อง unmount เนื้อหาเก่า
  7. เมื่อข้อมูลสำหรับผู้ใช้ 2 ถูกดึงมาและ UserProfile สามารถเรนเดอร์ได้สำเร็จ React จะ commit การอัปเดต และโปรไฟล์ใหม่จะปรากฏขึ้นอย่างราบรื่น

Transitions มอบการควบคุมชั้นสุดท้าย ช่วยให้คุณสร้างประสบการณ์การโหลดที่ซับซ้อนและเป็นมิตรกับผู้ใช้ซึ่งไม่เคยรู้สึกติดขัด

แนวทางปฏิบัติที่ดีที่สุดและข้อควรพิจารณาทั่วไป

บทสรุป

React Suspense เป็นมากกว่าแค่ฟีเจอร์ใหม่ มันคือวิวัฒนาการพื้นฐานในวิธีที่เราจัดการกับ asynchronicity ในแอปพลิเคชัน React โดยการเปลี่ยนจากการใช้แฟล็กการโหลดแบบ imperative ด้วยตนเองไปสู่โมเดลแบบ declarative เราสามารถเขียนคอมโพเนนต์ที่สะอาดขึ้น, ทนทานขึ้น, และง่ายต่อการประกอบกัน

ด้วยการรวม <Suspense> สำหรับสถานะ pending, Error Boundaries สำหรับสถานะล้มเหลว, และ useTransition สำหรับการอัปเดตที่ราบรื่น คุณจะมีชุดเครื่องมือที่สมบูรณ์และทรงพลังอยู่ในมือ คุณสามารถจัดการทุกอย่างตั้งแต่ loading spinner ง่ายๆ ไปจนถึงการเปิดเผยแดชบอร์ดแบบเหลื่อมเวลาที่ซับซ้อนด้วยโค้ดที่น้อยและคาดเดาได้ เมื่อคุณเริ่มนำ Suspense มาใช้ในโปรเจกต์ของคุณ คุณจะพบว่ามันไม่เพียงแต่ปรับปรุงประสิทธิภาพและประสบการณ์ผู้ใช้ของแอปพลิเคชันของคุณเท่านั้น แต่ยังช่วยลดความซับซ้อนของตรรกะการจัดการ state ของคุณได้อย่างมาก ทำให้คุณสามารถมุ่งเน้นไปที่สิ่งที่สำคัญอย่างแท้จริง: การสร้างฟีเจอร์ที่ยอดเยี่ยม