The `Suspense` Component (Part 1) #132
📖 Introduction
Following our detailed exploration of Route-based Code Splitting with React.lazy
(Part 2), where Suspense
played a crucial role as a fallback mechanism, this article focuses squarely on the Suspense
component itself. While its most common use case so far has been with React.lazy
, Suspense
is designed for more general asynchronous operations. In this first part, we'll revisit its core functionality with React.lazy
and explore creating more sophisticated fallback UIs like skeleton screens.
📚 Prerequisites
Before we begin, ensure you have a solid understanding of:
React.lazy
for code splitting (Articles 130 & 131).- Basic React components and JSX.
- The concept of asynchronous operations in JavaScript.
🎯 Article Outline: What You'll Master
In this article, you will learn:
- ✅ Recap of
Suspense
withReact.lazy
: Reinforcing its role in code splitting. - ✅ Designing Effective Fallback UIs: Moving beyond simple "Loading..." messages.
- ✅ Creating Skeleton Screens: Implementing placeholder UIs that mimic the actual content structure.
- ✅ Benefits of Skeleton Screens: How they improve perceived performance and user experience.
- ✅ Managing Multiple
Suspense
Boundaries: How nestedSuspense
components behave.
🧠 Section 1: Suspense
with React.lazy
- A Quick Refresher
As we've established, React.lazy()
allows you to define a component that React will load dynamically. When React attempts to render a lazy component for the first time, that component's code might not be available yet. This is an asynchronous operation.
React.Suspense
is the component React uses to manage this "suspended" state. It lets you specify a fallback
UI that React will render while it's waiting for the lazy component (or other async operations it might support in the future) to resolve.
import React, { Suspense } from 'react';
const MyLazyComponent = React.lazy(() => import('./MyLazyComponent'));
function App() {
return (
<div>
<h1>My Application</h1>
<Suspense fallback={<p>Loading component...</p>}>
<MyLazyComponent />
</Suspense>
</div>
);
}
Without Suspense
wrapping MyLazyComponent
, React would have no instruction on what to display during the load and would throw an error. The fallback
prop can be any valid React element, from simple text to complex components.
Key characteristics:
- Declarative: You declare what the loading state should look like.
- Catches Suspension: Any component further down the tree from a
Suspense
boundary that "suspends" (e.g., aReact.lazy
component that hasn't loaded yet) will be caught by the nearestSuspense
ancestor. - Not an Error Handler:
Suspense
is for managing loading states, not JavaScript errors. For errors during rendering or inReact.lazy
's dynamic import, you still need Error Boundaries (as discussed in Article 131).
💻 Section 2: Designing Effective Fallback UIs - Beyond Basic Text
A simple <div>Loading...</div>
is functional, but often not the best user experience, especially if the component takes a noticeable time to load. Users might perceive the application as slow or stuck. More sophisticated fallbacks can significantly improve perceived performance.
2.1 - Considerations for Good Fallbacks:
- Contextual Relevance: If possible, the fallback should hint at the content that is loading. A generic spinner for the whole page is less informative than a spinner within a specific section.
- Avoid Layout Shifts: The fallback UI should ideally occupy the same space as the component that will eventually render. This prevents the page content from "jumping" around when the actual component loads, which can be jarring.
- Performance of the Fallback Itself: The fallback UI should be lightweight and quick to render. Avoid complex logic or heavy assets in your loading indicators.
- User Engagement: For longer waits, consider more engaging loaders or progress indicators if appropriate, but for typical lazy-loading scenarios, subtle is often better.
2.2 - Simple Spinner Components
A common improvement is to use a dedicated spinner component.
// components/Spinner.jsx
import React from 'react';
import './Spinner.css'; // For spinner styles
const Spinner = ({ size = 'medium' }) => {
return <div className={`spinner spinner-${size}`}></div>;
};
export default Spinner;
// Spinner.css (Example)
/*
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
width: 36px;
height: 36px;
border-radius: 50%;
border-left-color: #09f;
animation: spin 1s ease infinite;
}
.spinner-small { width: 20px; height: 20px; border-width: 2px; }
.spinner-large { width: 50px; height: 50px; border-width: 5px; }
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
*/
Using it with Suspense
:
// App.jsx
import React, { Suspense } from 'react';
import Spinner from './components/Spinner';
const UserProfile = React.lazy(() => import('./UserProfile'));
function App() {
return (
<Suspense fallback={<Spinner size="large" />}>
<UserProfile />
</Suspense>
);
}
This provides a more standard visual cue that something is loading.
🛠️ Section 3: Implementing Skeleton Screens
Skeleton screens (also known as placeholder UIs) take the fallback concept a step further. They display a simplified, static version of the UI that mimics the structure and layout of the content that is about to load.
Benefits of Skeleton Screens:
- Improved Perceived Performance: Users see a structure that resembles the final content, making it feel like the application is loading faster, even if the actual load time is the same.
- Reduced Layout Shift: Because the skeleton often matches the dimensions of the final content, there's less jarring page reflow when the real content appears.
- Manages Expectations: It gives users a visual cue of what kind of content to expect.
- Focus on Content Structure: It draws attention to the layout and hierarchy rather than just a blank space or a generic spinner.
3.1 - Example: Skeleton for a User Profile Card
Let's say we have a UserProfileCard
component we want to lazy load:
// components/UserProfileCard.jsx (Actual component)
import React from 'react';
import './UserProfileCard.css';
const UserProfileCard = ({ user }) => {
if (!user) return null; // Or some other handling for no user data
return (
<div className="user-profile-card">
<img src={user.avatarUrl} alt={`${user.name}'s avatar`} className="profile-avatar" />
<div className="profile-info">
<h2 className="profile-name">{user.name}</h2>
<p className="profile-bio">{user.bio}</p>
<a href={`mailto:${user.email}`} className="profile-email">{user.email}</a>
</div>
</div>
);
};
export default UserProfileCard;
/* UserProfileCard.css (Illustrative)
.user-profile-card { display: flex; border: 1px solid #ccc; padding: 15px; border-radius: 8px; background-color: #f9f9f9; align-items: center; }
.profile-avatar { width: 80px; height: 80px; border-radius: 50%; margin-right: 15px; object-fit: cover; }
.profile-info { display: flex; flex-direction: column; }
.profile-name { margin: 0 0 5px 0; font-size: 1.5em; }
.profile-bio { margin: 0 0 10px 0; font-size: 0.9em; color: #555; }
.profile-email { font-size: 0.9em; color: #007bff; }
*/
Now, let's create a skeleton component for it:
// components/UserProfileCardSkeleton.jsx
import React from 'react';
import './UserProfileCardSkeleton.css';
const UserProfileCardSkeleton = () => {
return (
<div className="user-profile-card-skeleton">
<div className="skeleton-avatar"></div>
<div className="skeleton-info">
<div className="skeleton-line skeleton-name"></div>
<div className="skeleton-line skeleton-bio"></div>
<div className="skeleton-line skeleton-bio short"></div>
<div className="skeleton-line skeleton-email"></div>
</div>
</div>
);
};
export default UserProfileCardSkeleton;
/* UserProfileCardSkeleton.css (Illustrative)
.user-profile-card-skeleton { display: flex; border: 1px solid #eee; padding: 15px; border-radius: 8px; background-color: #fff; align-items: center; }
.skeleton-avatar { width: 80px; height: 80px; border-radius: 50%; margin-right: 15px; background-color: #e0e0e0; animation: pulse 1.5s infinite ease-in-out; }
.skeleton-info { display: flex; flex-direction: column; flex-grow: 1; }
.skeleton-line { height: 1em; background-color: #e0e0e0; border-radius: 4px; margin-bottom: 8px; animation: pulse 1.5s infinite ease-in-out; }
.skeleton-name { width: 60%; height: 1.5em; margin-bottom: 10px; }
.skeleton-bio { width: 90%; }
.skeleton-bio.short { width: 70%; }
.skeleton-email { width: 50%; height: 0.9em; }
.skeleton-line:last-child { margin-bottom: 0; }
@keyframes pulse {
0% { background-color: #e0e0e0; }
50% { background-color: #f0f0f0; }
100% { background-color: #e0e0e0; }
}
*/
3.2 - Using the Skeleton Screen with Suspense
Now, we can use UserProfileCardSkeleton
as the fallback for our lazy-loaded UserProfileCard
.
// App.jsx
import React, { Suspense, useState, useEffect } from 'react';
import UserProfileCardSkeleton from './components/UserProfileCardSkeleton';
// Assume Spinner is also available for other fallbacks or initial page load
// import Spinner from './components/Spinner';
const LazyUserProfileCard = React.lazy(() => import('./components/UserProfileCard'));
function App() {
const [user, setUser] = useState(null);
const [isLoadingUser, setIsLoadingUser] = useState(true);
// Simulate fetching user data for the profile card
useEffect(() => {
setTimeout(() => { // Simulate API call delay
setUser({
name: 'Alice Wonderland',
bio: 'Curious explorer of digital realms. Enjoys tea parties with mad hatters and coding challenges.',
email: '[email protected]',
avatarUrl: 'https://source.unsplash.com/random/100x100/?woman,portrait', // Placeholder image
});
setIsLoadingUser(false);
}, 3000); // User data loads after 3 seconds
}, []);
// This outer Suspense could be for the whole page or a larger section
// if App itself was lazy loaded or fetching initial app-wide data.
// For this specific example, we are focusing on LazyUserProfileCard.
if (isLoadingUser && !user) {
// If user data is loading (simulated by isLoadingUser) AND we don't have user data yet,
// we might show a page-level spinner or a more general skeleton for the whole section
// where the profile card will appear. Or, we can rely on the Suspense fallback below.
// For simplicity here, we'll just show the skeleton via Suspense.
}
return (
<div style={{ padding: '20px', maxWidth: '500px', margin: '0 auto' }}>
<h1>User Profile</h1>
<Suspense fallback={<UserProfileCardSkeleton />}>
{/*
LazyUserProfileCard will suspend if its code isn't loaded.
Once loaded, it will render. If `user` prop is null initially,
it might render nothing or its own internal loading/empty state.
The skeleton is shown while the *code* for LazyUserProfileCard is loading.
*/}
{!isLoadingUser && user && <LazyUserProfileCard user={user} />}
{isLoadingUser && <UserProfileCardSkeleton />} {/* Optionally show skeleton also while data is fetching, if desired */}
</Suspense>
<p style={{marginTop: '20px', fontSize: '0.8em', color: '#777'}}>
Note: The UserProfileCard component itself is lazy-loaded.
The user data for the card is simulated to load after 3 seconds.
You'll see the skeleton while the component code loads, and potentially
if data is also being fetched and handled by the parent.
</p>
</div>
);
}
export default App;
Explanation of Behavior:
- When
App
renders, React encounters<Suspense fallback={<UserProfileCardSkeleton />}>
. - Inside it,
LazyUserProfileCard
is referenced. If its code hasn't been loaded yet (first time or cache miss),LazyUserProfileCard
"suspends." Suspense
catches this and rendersUserProfileCardSkeleton
.- Meanwhile, the code for
LazyUserProfileCard.js
is downloaded. - Once downloaded,
LazyUserProfileCard
can render. In our example, it will render if!isLoadingUser && user
is true. - The
useEffect
simulates fetching user data, which takes 3 seconds.- If the code for
LazyUserProfileCard
loads before the user data is ready,LazyUserProfileCard
might render an empty state or its own internal loader ifuser
is null. - The line
{isLoadingUser && <UserProfileCardSkeleton />}
demonstrates an optional way to also show the skeleton while data is being fetched, even if the component code is loaded. This ensures a consistent placeholder.
- If the code for
The key is that the Suspense
fallback handles the code loading aspect of React.lazy
. Data loading is a separate concern that you manage with state (useState
, useEffect
, data fetching libraries, or future Concurrent Mode features).
🔬 Section 4: Managing Multiple and Nested Suspense
Boundaries
As mentioned in Article 131, you can have multiple Suspense
components. The nearest Suspense
ancestor will catch a suspending component.
4.1 - Sibling Suspense
Boundaries:
If you have multiple independent lazy components at the same level, each can have its own Suspense
boundary.
function Dashboard() {
const UserWidget = React.lazy(() => import('./UserWidget'));
const SalesChart = React.lazy(() => import('./SalesChart'));
return (
<div>
<section>
<h2>User Information</h2>
<Suspense fallback={<WidgetSkeleton type="user" />}>
<UserWidget />
</Suspense>
</section>
<section>
<h2>Sales Data</h2>
<Suspense fallback={<WidgetSkeleton type="chart" />}>
<SalesChart />
</Suspense>
</section>
</div>
);
}
Here, UserWidget
and SalesChart
load independently, each showing its own skeleton.
4.2 - Nested Suspense
Boundaries:
A child component might also use React.lazy
and have its own Suspense
boundary, even if it's rendered within an outer Suspense
boundary.
// ParentComponent.jsx
import React, { Suspense } from 'react';
const LazyChildWithOwnSuspense = React.lazy(() => import('./LazyChildWithOwnSuspense'));
function ParentComponent() {
return (
<Suspense fallback={<div>Loading Parent Section...</div>}>
<h1>Parent Section Title</h1>
<LazyChildWithOwnSuspense />
</Suspense>
);
}
// LazyChildWithOwnSuspense.jsx
import React, { Suspense } from 'react';
const GrandChildComponent = React.lazy(() => import('./GrandChildComponent'));
function LazyChildWithOwnSuspense() {
return (
<div style={{ marginLeft: '20px', border: '1px solid blue', padding: '10px' }}>
<h2>Lazy Child Section</h2>
<Suspense fallback={<div>Loading Grandchild... (more specific)</div>}>
<GrandChildComponent />
</Suspense>
</div>
);
}
export default LazyChildWithOwnSuspense;
// GrandChildComponent.jsx
// ... (imagine this is a component that takes a moment to load its code)
Behavior:
- If
LazyChildWithOwnSuspense
code is loading, the outerSuspense
(inParentComponent
) shows "Loading Parent Section...". - Once
LazyChildWithOwnSuspense
code is loaded, it renders. - If
GrandChildComponent
code is then loading, the innerSuspense
(inLazyChildWithOwnSuspense
) shows "Loading Grandchild...". The "Loading Parent Section..." fallback is no longer active for this part of the tree.
This allows for fine-grained control over loading states at different levels of the component hierarchy.
💡 Conclusion & Key Takeaways (Part 1)
Suspense
is a powerful component for declaratively managing loading states, especially when paired with React.lazy
. Moving beyond simple text fallbacks to implementing informative skeleton screens can significantly enhance the user's perception of your application's performance. Understanding how multiple Suspense
boundaries interact allows for crafting sophisticated and granular loading experiences.
Key Takeaways So Far:
- Effective
Suspense
fallbacks go beyond basic text; consider spinners and skeleton screens. - Skeleton screens improve perceived performance by mimicking the final content structure during load.
- Multiple
Suspense
components can be used to create fine-grained loading states for different parts of the UI. - The nearest
Suspense
ancestor catches a suspending component.
Challenge Yourself:
Take the Dashboard
example with UserWidget
and SalesChart
. Design and implement simple WidgetSkeleton
components for both, making them visually distinct. Observe how they load independently.
➡️ Next Steps
We've delved into creating better fallback UIs with Suspense
, particularly skeleton screens. In "The Suspense
Component (Part 2)", we will explore:
- Transitions with
useTransition
to keep the UI interactive while new content is loading in the background. - The future potential of
Suspense
for data fetching and other asynchronous operations. - Patterns and best practices for using
Suspense
in larger applications.
Continue refining your understanding of Suspense
to build truly polished React experiences!
glossary
- Skeleton Screen (Placeholder UI): A type of fallback UI that displays a simplified, static version of the interface, mimicking the structure of the content that is about to load. It improves perceived performance.
- Layout Shift: An undesirable effect where page content visibly moves or "jumps" as resources load, often caused by elements rendering and then changing size. Skeleton screens can help minimize this.
- Perceived Performance: How fast a user feels an application is operating, which can be different from the actual measured load time. Good fallback UIs improve perceived performance.
- Fine-grained Loading States: Providing specific loading indicators for smaller, individual parts of a UI rather than a single generic loader for a large section.