React Suspense
Handle loading states declaratively in React
⏳ What is React Suspense?
React Suspense lets you display fallback content while waiting for components to load. It handles loading states declaratively, making code cleaner by eliminating manual loading flags for lazy-loaded components and data fetching.
import { Suspense, lazy } from 'react';
const LazyComponent = lazy(() => import('./Component'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
Key Suspense Concepts
Suspense
Wrap components that might suspend
<Suspense fallback={...}>
<Component />
</Suspense>
lazy()
Dynamically import components
const Comp = lazy(
() => import('./Comp')
);
Fallback
Show loading UI while waiting
fallback={
<Spinner />
}
Code Splitting
Load code only when needed
// Reduces initial bundle
lazy(() => import(...))
🔹 Basic Suspense with Lazy Loading
Lazy loading with Suspense splits your code into smaller chunks that load on demand. This reduces initial bundle size and improves performance by only loading components when they're actually needed by the user.
// App.jsx
import { Suspense, lazy } from 'react';
// Lazy load the component
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<div>
<h1>My App</h1>
<Suspense fallback={<div>Loading component...</div>}>
<HeavyComponent />
</Suspense>
</div>
);
}
export default App;
// HeavyComponent.jsx
function HeavyComponent() {
return (
<div>
<h2>Heavy Component</h2>
<p>This component was lazy loaded!</p>
</div>
);
}
export default HeavyComponent;
🔹 Custom Loading Spinner
Create reusable loading components for better user experience. Custom spinners provide visual feedback during loading, making your application feel more responsive and professional while components or data are being fetched.
// LoadingSpinner.jsx
function LoadingSpinner() {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '200px'
}}>
<div style={{
border: '4px solid #f3f3f3',
borderTop: '4px solid #3498db',
borderRadius: '50%',
width: '40px',
height: '40px',
animation: 'spin 1s linear infinite'
}} />
</div>
);
}
export default LoadingSpinner;
/* Add to your CSS */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
// Usage with Suspense
import { Suspense, lazy } from 'react';
import LoadingSpinner from './LoadingSpinner';
const Dashboard = lazy(() => import('./Dashboard'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Dashboard />
</Suspense>
);
}
🔹 Multiple Lazy Components
You can lazy load multiple components within a single Suspense boundary. All components share the same fallback, showing one loading state until all lazy components are ready, simplifying loading state management.
// App.jsx
import { Suspense, lazy } from 'react';
const Header = lazy(() => import('./Header'));
const Sidebar = lazy(() => import('./Sidebar'));
const Content = lazy(() => import('./Content'));
function App() {
return (
<div>
<Suspense fallback={<div>Loading page...</div>}>
<Header />
<div style={{ display: 'flex' }}>
<Sidebar />
<Content />
</div>
</Suspense>
</div>
);
}
export default App;
🔹 Nested Suspense Boundaries
Multiple Suspense boundaries allow different parts of your UI to load independently. This creates a better user experience by showing content progressively rather than waiting for everything to load before displaying anything.
// App.jsx
import { Suspense, lazy } from 'react';
const Header = lazy(() => import('./Header'));
const MainContent = lazy(() => import('./MainContent'));
const Sidebar = lazy(() => import('./Sidebar'));
function App() {
return (
<div>
{/* Header loads independently */}
<Suspense fallback={<div>Loading header...</div>}>
<Header />
</Suspense>
<div style={{ display: 'flex' }}>
{/* Main content loads independently */}
<Suspense fallback={<div>Loading content...</div>}>
<MainContent />
</Suspense>
{/* Sidebar loads independently */}
<Suspense fallback={<div>Loading sidebar...</div>}>
<Sidebar />
</Suspense>
</div>
</div>
);
}
export default App;
🔹 Route-Based Code Splitting
Combine React Router with Suspense for route-based code splitting. Each route loads only when visited, dramatically reducing initial bundle size and improving performance for applications with many pages or complex routes.
// App.jsx
import { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import LoadingSpinner from './LoadingSpinner';
// Lazy load route components
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
export default App;
🔹 Error Boundaries with Suspense
Combine Error Boundaries with Suspense to handle both loading and error states gracefully. This provides a complete solution for asynchronous component loading, catching errors that occur during lazy loading or component rendering.
// ErrorBoundary.jsx
import { Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>Something went wrong.</h2>
<button onClick={() => window.location.reload()}>
Reload
</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
// App.jsx with Error Boundary
import { Suspense, lazy } from 'react';
import ErrorBoundary from './ErrorBoundary';
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<ErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
);
}
🔹 Best Practices
Follow these guidelines to use Suspense effectively in your applications:
- Strategic Code Splitting: Split at route level first, then component level for large features
- Meaningful Fallbacks: Show skeleton screens or spinners that match your UI design
- Avoid Over-Splitting: Don't lazy load small components; it adds overhead
- Preload Important Routes: Use link prefetching for critical user paths
- Error Handling: Always wrap Suspense with Error Boundaries
- Test Loading States: Throttle network in dev tools to see loading UI