React Suspense: Chấm Dứt Nỗi Ám Ảnh Của Loading Spinner!
Thành thật mà nói, không ai thích phải chờ đợi trong khi nhìn chằm chằm vào một loading spinner khi trang web đang tải. Nó giống như xem sơn khô – ngoại trừ việc sơn là trang web của bạn và sự khô khan là sự kiên nhẫn của người dùng. React Suspense ra đời để cứu vãn tình hình (và giữ cho tâm trí người dùng không bị “điên”) bằng cách cho phép lazy loading, tức là tải các component và dữ liệu theo nhu cầu, không phải tất cả cùng lúc. Bài viết này sẽ đi sâu vào cách sử dụng Suspense thực tế, giúp bạn cải thiện thời gian tải ban đầu và tạo ra trải nghiệm người dùng mượt mà hơn.
Lazy Loading: Chìa Khóa Để Đạt Được Hiệu Năng Tối Ưu
Lazy loading giống như việc gọi món ăn tại nhà hàng. Bạn không nhận tất cả món trong thực đơn một lúc, mà gọi từng món khi cần thiết. Tương tự, với lazy loading, các component và dữ liệu chỉ được tải khi chúng thực sự cần, thay vì tải tất cả ngay từ đầu. Điều này có thể cải thiện đáng kể thời gian tải ban đầu, đặc biệt là đối với các ứng dụng lớn và phức tạp.
Suspense Trong Thực Tế: Ví Dụ Mã (Với TypeScript)
Suspense cực kỳ dễ sử dụng. Bạn chỉ cần bọc component muốn lazy-load trong một lệnh gọi React.lazy()
và sau đó bọc nó trong component <Suspense>
:
import React, { Suspense } from "react";
// Lazy-load component OtherComponent
const OtherComponent = React.lazy(() => import("./OtherComponent"));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
Prop fallback
của component Suspense
xác định giao diện sẽ được hiển thị trong khi OtherComponent
đang tải. Đây có thể là một loading spinner, một placeholder, hoặc bất kỳ thứ gì bạn muốn.
Ví Dụ Thực Tế: Lazy Loading Một Component Nặng
Giả sử bạn có một component vẽ các biểu đồ và hình ảnh phức tạp. Component này rất nặng và mất một khoảng thời gian để tải. Bạn có thể sử dụng Suspense để tải nó chỉ khi cần thiết:
import React, { Suspense, useState, useEffect } from "react";
// Lazy-load component ChartComponent
const ChartComponent = React.lazy(() => import("./ChartComponent"));
function Dashboard() {
const [data, setData] = useState(null);
useEffect(() => {
async function fetchData() {
const result = await fetch("https://example.com/api/data");
const json = await result.json();
setData(json);
}
fetchData();
}, []);
if (!data) {
return <div>Loading Dashboard...</div>;
}
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading Charts...</div>}>
<ChartComponent data={data} />
</Suspense>
</div>
);
}
Trong ví dụ này, ChartComponent
chỉ được tải khi dữ liệu dashboard có sẵn, mang đến trải nghiệm người dùng mượt mà.
Fetch Dữ Liệu Với Suspense: Giữ Cho Việc Tải Dữ Liệu Bất Đồng Bộ
Suspense cũng hỗ trợ việc tải dữ liệu bất đồng bộ. Bạn có thể gói logic tải dữ liệu của mình trong một promise, và Suspense sẽ tự động xử lý trạng thái tải.
import React, { Suspense, useState, useEffect } from "react";
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
async function fetchPostData(): Promise<Post[]> {
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
return res.json();
}
function PostList() {
const [posts, setPosts] = useState<Post[] | null>(null);
useEffect(() => {
let ignore = false;
async function startFetching() {
const posts = await fetchPostData();
if (!ignore) {
setPosts(posts);
}
}
startFetching();
return () => {
ignore = true;
};
}, []);
if (!posts) {
throw fetchPostData();
}
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<h3>{post.title}</h3>
</li>
))}
</ul>
);
}
function MyComponent2() {
return (
<Suspense fallback={<div>Loading Posts...</div>}>
<PostList />
</Suspense>
);
}
export default MyComponent2;
Trong ví dụ này, component PostList
sẽ “ném” một promise nếu dữ liệu chưa tải xong. Suspense sẽ bắt promise này và hiển thị giao diện fallback cho đến khi promise hoàn thành.
Ví Dụ Thực Tế: Lazy Loading Dữ Liệu Với Suspense
Giả sử bạn có một news feed mà tải các bài viết bất đồng bộ. Bạn có thể sử dụng Suspense để xử lý trạng thái tải một cách mượt mà:
import React, { Suspense, useState, useEffect } from "react";
interface Article {
id: number;
title: string;
body: string;
}
async function fetchArticles(): Promise<Article[]> {
const res = await fetch("https://example.com/api/articles");
return res.json();
}
function ArticleList() {
const [articles, setArticles] = useState<Article[] | null>(null);
useEffect(() => {
let ignore = false;
async function startFetching() {
const articles = await fetchArticles();
if (!ignore) {
setArticles(articles);
}
}
startFetching();
return () => {
ignore = true;
};
}, []);
if (!articles) {
throw fetchArticles();
}
return (
<ul>
{articles.map((article) => (
<li key={article.id}>
<h3>{article.title}</h3>
<p>{article.body}</p>
</li>
))}
</ul>
);
}
function NewsFeed() {
return (
<div>
<h1>News Feed</h1>
<Suspense fallback={<div>Loading Articles...</div>}>
<ArticleList />
</Suspense>
</div>
);
}
export default NewsFeed;
Trong ví dụ này, ArticleList
sẽ “ném” một promise nếu bài viết chưa được tải xong, và Suspense sẽ xử lý trạng thái tải bằng cách hiển thị giao diện fallback.
Lazy Loading Dựa Trên Route: Tối Ưu Hóa Quá Trình Điều Hướng
Suspense rất hữu ích cho lazy loading dựa trên route, nơi các component của từng route chỉ được tải khi người dùng điều hướng đến các route đó. Điều này có thể cải thiện hiệu suất rất nhiều đối với các ứng dụng lớn với nhiều route.
Sử Dụng React Router Với Suspense
React Router tích hợp dễ dàng với Suspense để lazy load các component:
import React, { lazy, Suspense } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
const Home = lazy(() => import("./Home"));
const About = lazy(() => import("./About"));
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
</Switch>
</Suspense>
</Router>
);
}
export default App;
Trong ví dụ này, các component Home
và About
sẽ được tải lazy khi người dùng truy cập vào các route tương ứng.
Xử Lý Lỗi Với Suspense: Bắt Những Lỗi Phiền Phức
Suspense cũng có thể được sử dụng để xử lý lỗi. Nếu một component được lazy-load ném ra lỗi trong quá trình render, Suspense có thể bắt lỗi và hiển thị giao diện fallback. Tuy nhiên, để xử lý lỗi một cách hiệu quả hơn, đặc biệt đối với những lỗi xảy ra sau khi render ban đầu, tốt nhất là bạn nên sử dụng Error Boundaries (chúng tôi sẽ đề cập đến trong bài viết khác!).
Implementing Error Handling with Suspense
import React, { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
const OtherComponent = React.lazy(() => import("./OtherComponent"));
function ErrorFallback({
error,
resetErrorBoundary,
}: {
error: Error;
resetErrorBoundary: () => void;
}) {
return (
<div>
<h1>Something went wrong!</h1>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// Reset lại trạng thái của ứng dụng
}}
>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</ErrorBoundary>
);
}
export default App;
Trong ví dụ này, nếu OtherComponent
ném ra lỗi, ErrorBoundary
sẽ bắt lỗi và hiển thị thông báo lỗi thân thiện với người dùng.
Suspense và Concurrent Mode: Một Sự Kết Hợp Mạnh Mẽ
Suspense hoạt động rất tốt với Concurrent Mode (hiện nay đã tích hợp vào React 18 như chế độ mặc định), cho phép React gián đoạn quá trình render và ưu tiên các cập nhật quan trọng hơn, chẳng hạn như các tương tác của người dùng. Điều này giúp tạo ra trải nghiệm người dùng mượt mà hơn.
Concurrent Mode Nâng Cao Suspense
Concurrent Mode giúp React thực hiện các cập nhật chi tiết hơn, cho phép React xử lý các tác vụ như render, tải dữ liệu, và cập nhật UI hiệu quả hơn. Điều này đặc biệt hữu ích khi kết hợp với Suspense, vì nó cho phép React ưu tiên việc render và tải dựa trên các tương tác của người dùng, nâng cao tính phản hồi của ứng dụng.
Best Practices Khi Sử Dụng Suspense
- Giữ Fallback Đơn Giản: Không nên để logic quá phức tạp hoặc giao diện UI phức tạp trong phần fallback. Mục tiêu là cung cấp một placeholder nhanh chóng trong khi tải nội dung.
- Dùng Suspense Ở Nhiều Cấp Độ: Đừng chỉ bọc toàn bộ ứng dụng trong một
Suspense
duy nhất. Hãy sử dụng nó một cách chiến lược tại các cấp độ khác nhau của cây component để tối ưu hóa việc tải cho từng phần của UI. - Lưu Ý Đến Trải Nghiệm Người Dùng: Cân nhắc xem fallback UI nào sẽ phù hợp nhất cho ứng dụng của bạn và người dùng. Một loading spinner có thể ổn trong một số trường hợp, nhưng skeleton loader hoặc placeholder có thể phù hợp hơn trong những trường hợp khác.
Ví Dụ: Sử Dụng Nhiều Boundaries Suspense
import React, { Suspense, lazy } from "react";
const Header = lazy(() => import("./Header"));
const MainContent = lazy(() => import("./MainContent"));
const Footer = lazy(() => import("./Footer"));
function App() {
return (
<div>
<Suspense fallback={<div>Loading Header...</div>}>
<Header />
</Suspense>
<Suspense fallback={<div>Loading Main Content...</div>}>
<MainContent />
</Suspense>
<Suspense fallback={<div>Loading Footer...</div>}>
<Footer />
</Suspense>
</div>
);
}
export default App;
Trong ví dụ này, mỗi phần của ứng dụng có một boundary Suspense riêng, giúp kiểm soát chi tiết trạng thái tải và tạo trải nghiệm người dùng tốt hơn.
Kết Luận: Lazy Loading Cho Những Tính Năng Mượt Mà
React Suspense là một công cụ mạnh mẽ để lazy load các component và dữ liệu, giúp cải thiện hiệu suất và trải nghiệm người dùng của ứng dụng. Bằng cách sử dụng Suspense một cách chiến lược và tuân thủ các best practices, bạn có thể tạo ra những ứng dụng web nhanh chóng, phản hồi tốt và dễ sử dụng. Không còn nữa cảnh loading spinner đáng sợ!