How to properly use React.lazy() with Suspense?

👨‍💻 Frontend Developer 🟡 Often Asked 🎚️ Hard
#React

Short Answer

React.lazy() must always be used with Suspense — this is a mandatory requirement. Suspense provides fallback UI while the lazy component is loading. Without Suspense, the application will throw an error.

Proper usage:

// Create lazy component
const LazyComponent = React.lazy(() => import('./LazyComponent'));
 
// Must wrap in Suspense
function App() {
  return (
    <React.Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </React.Suspense>
  );
}

Full Answer

React.lazy() and Suspense work together to implement code splitting. Suspense is a mechanism that “suspends” component rendering until the required resources are loaded.

What Happens Without Suspense?

Rendering Error

// ❌ WITHOUT Suspense - app will crash with error
function App() {
  const LazyComponent = React.lazy(() => import('./LazyComponent'));
  
  return (
    <div>
      <h1>My App</h1>
      {/* Error: A React component suspended while rendering */}
      <LazyComponent />
    </div>
  );
}

What Happens Under the Hood

// React.lazy returns a special object
const LazyComponent = React.lazy(() => import('./Component'));
 
// This object "throws" a Promise on first render
// Suspense catches this Promise and shows fallback
// Without Suspense, Promise is not caught = error

Multiple Lazy Components in One Suspense

Basic Example

const Header = React.lazy(() => import('./Header'));
const Content = React.lazy(() => import('./Content'));
const Footer = React.lazy(() => import('./Footer'));
 
function App() {
  return (
    // ✅ Can wrap multiple components
    <React.Suspense fallback={<div>Loading app...</div>}>
      <Header />
      <Content />
      <Footer />
    </React.Suspense>
  );
}

Loading Behavior

function Dashboard() {
  // All three components will start loading in parallel
  const Stats = React.lazy(() => import('./Stats'));
  const Charts = React.lazy(() => import('./Charts'));
  const Table = React.lazy(() => import('./Table'));
  
  return (
    <React.Suspense fallback={<DashboardSkeleton />}>
      {/* Fallback shown until ALL components are loaded */}
      <Stats />
      <Charts />
      <Table />
    </React.Suspense>
  );
}

Strategies for Multiple Suspense Boundaries

1. Granular Loading

function App() {
  return (
    <div>
      {/* Each component loads independently */}
      <React.Suspense fallback={<HeaderSkeleton />}>
        <Header />
      </React.Suspense>
      
      <React.Suspense fallback={<MainSkeleton />}>
        <MainContent />
      </React.Suspense>
      
      <React.Suspense fallback={<SidebarSkeleton />}>
        <Sidebar />
      </React.Suspense>
    </div>
  );
}

2. Nested Suspense Boundaries

function App() {
  return (
    // Outer Suspense for critical components
    <React.Suspense fallback={<AppLoader />}>
      <Layout>
        <Header />
        
        {/* Inner Suspense for content */}
        <React.Suspense fallback={<ContentLoader />}>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/profile" element={<Profile />} />
          </Routes>
        </React.Suspense>
        
        <Footer />
      </Layout>
    </React.Suspense>
  );
}

3. Waterfall vs Parallel Loading

// ❌ Waterfall - components load sequentially
function WaterfallLoading() {
  const [showSecond, setShowSecond] = useState(false);
  
  return (
    <>
      <React.Suspense fallback={<div>Loading first...</div>}>
        <FirstComponent onLoad={() => setShowSecond(true)} />
      </React.Suspense>
      
      {showSecond && (
        <React.Suspense fallback={<div>Loading second...</div>}>
          <SecondComponent />
        </React.Suspense>
      )}
    </>
  );
}
 
// ✅ Parallel loading - more efficient
function ParallelLoading() {
  return (
    <React.Suspense fallback={<div>Loading components...</div>}>
      <FirstComponent />
      <SecondComponent />
    </React.Suspense>
  );
}

What Happens When Component Fails to Load?

Without Error Handling

// If component fails to load, Suspense won't help
function App() {
  return (
    <React.Suspense fallback={<div>Loading...</div>}>
      {/* If loading fails - white screen */}
      <LazyComponent />
    </React.Suspense>
  );
}

Proper Handling with Error Boundary

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }
  
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  
  componentDidCatch(error, errorInfo) {
    // Log error
    console.error('Chunk loading error:', error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <div className="error-fallback">
          <h2>Failed to&nbsp;load component</h2>
          <button onClick={() => window.location.reload()}>
            Reload Page
          </button>
        </div>
      );
    }
    
    return this.props.children;
  }
}
 
// Usage
function App() {
  return (
    <ErrorBoundary>
      <React.Suspense fallback={<Loader />}>
        <LazyComponent />
      </React.Suspense>
    </ErrorBoundary>
  );
}

Advanced Error Handling Patterns

1. Automatic Retry

function createLazyWithRetry(componentImport) {
  return React.lazy(() =>
    componentImport().catch((error) => {
      // Check if this is a chunk loading error
      if (error.name === 'ChunkLoadError') {
        // Try loading again
        return new Promise((resolve) => {
          setTimeout(() => {
            resolve(componentImport());
          }, 1500);
        });
      }
      throw error;
    })
  );
}
 
// Usage
const RobustLazyComponent = createLazyWithRetry(
  () => import('./HeavyComponent')
);

2. Retryable Error Boundary

function RetryableErrorBoundary({ children }) {
  const [hasError, setHasError] = useState(false);
  const [retryCount, setRetryCount] = useState(0);
  
  const resetError = () => {
    setHasError(false);
    setRetryCount(prev => prev + 1);
  };
  
  if (hasError) {
    return (
      <div className="error-container">
        <h3>Loading Error</h3>
        <p>Failed to&nbsp;load required resources</p>
        <button onClick={resetError}>
          Try Again ({retryCount})
        </button>
      </div>
    );
  }
  
  return (
    <ErrorBoundary onError={() => setHasError(true)}>
      {/* Force re-mount on retry */}
      <React.Fragment key={retryCount}>
        {children}
      </React.Fragment>
    </ErrorBoundary>
  );
}

Optimizing UX with Suspense

1. Delayed Fallback Display

function DelayedSuspense({ delay = 300, fallback, children }) {
  const [showFallback, setShowFallback] = useState(false);
  
  useEffect(() => {
    const timer = setTimeout(() => {
      setShowFallback(true);
    }, delay);
    
    return () => clearTimeout(timer);
  }, [delay]);
  
  return (
    <React.Suspense fallback={showFallback ? fallback : null}>
      {children}
    </React.Suspense>
  );
}
 
// Usage - fallback appears only if loading > 300ms
<DelayedSuspense fallback={<Spinner />}>
  <LazyComponent />
</DelayedSuspense>

2. Progressive Indicators

function ProgressiveFallback() {
  const [stage, setStage] = useState(0);
  
  useEffect(() => {
    const timers = [
      setTimeout(() => setStage(1), 500),   // "Loading..."
      setTimeout(() => setStage(2), 2000),  // "Almost there..."
      setTimeout(() => setStage(3), 5000),  // "Taking longer..."
    ];
    
    return () => timers.forEach(clearTimeout);
  }, []);
  
  const messages = [
    "Loading...",
    "Almost there...",
    "This is&nbsp;taking longer than usual...",
    "Please check your connection..."
  ];
  
  return (
    <div className="progressive-loader">
      <Spinner />
      <p>{messages[stage]}</p>
    </div>
  );
}

Best Practices

1. Group by Functionality

// ✅ Good - logically related components
<React.Suspense fallback={<DashboardSkeleton />}>
  <DashboardHeader />
  <DashboardStats />
  <DashboardCharts />
</React.Suspense>
 
// ❌ Bad - unrelated components
<React.Suspense fallback={<div>Loading...</div>}>
  <UserProfile />
  <WeatherWidget />
  <StockTicker />
</React.Suspense>

2. Appropriate Fallback Components

// ✅ Good - skeleton matches content
<React.Suspense fallback={<ArticleSkeleton />}>
  <Article />
</React.Suspense>
 
// ❌ Bad - generic spinner for everything
<React.Suspense fallback={<Spinner />}>
  <ComplexDashboard />
</React.Suspense>

3. Strategic Boundary Placement

function App() {
  return (
    <>
      {/* Critical components without Suspense */}
      <NavigationBar />
      
      {/* Main content with Suspense */}
      <React.Suspense fallback={<MainSkeleton />}>
        <Routes>
          <Route path="/*" element={<MainApp />} />
        </Routes>
      </React.Suspense>
      
      {/* Non-critical components with separate Suspense */}
      <React.Suspense fallback={null}>
        <Analytics />
        <ChatWidget />
      </React.Suspense>
    </>
  );
}

Common Mistakes and Solutions

  1. Forgot Suspense — always wrap lazy components
  2. Too many Suspense boundaries — group logically related components
  3. No error handling — use Error Boundaries
  4. Blocking fallbacks — don’t show modals as fallback
  5. Ignoring network conditions — test on slow connections

React.lazy() and Suspense are a powerful combination for load optimization, but require proper usage to ensure good user experience.


Want more interview preparation articles? Subscribe to EasyAdvice, bookmark the site and improve every day 💪