How to Create a Lazy-Loaded Component in React

Intermediate8 min interview
Skills tested:
Using React.lazy with a dynamic import to split a component into a separate chunkWrapping lazy components with Suspense and a fallbackPairing lazy with an ErrorBoundary to handle chunk load failuresAdapting named exports with a .then() wrapperKnowing that lazy loading is route-level code splitting and when to apply it

Advertisement

🧩 Scenario

Lazy loading is a practical performance question. Interviewers look for whether you know the Suspense requirement, the ErrorBoundary pairing, and the named-export gotcha — not just that React.lazy exists.

Architecture Walkthrough

What Lazy Loading Does

Without code splitting, webpack (or any bundler) includes every imported module in a single JavaScript bundle. The browser must download, parse, and execute the entire bundle before React can render anything. For a large app, this bundle can be hundreds of kilobytes, most of it for routes and features the current user may never visit.

React.lazy converts a component import from static (included at build time) to dynamic (fetched on demand at runtime). Bundlers treat dynamic import() calls as split points and generate a separate chunk file for that module. When React first encounters the lazy component in the render tree, it triggers the import(), waits for the chunk to download, and renders the component. The browser only fetches the chunk file for a route when that route is first visited.

Suspense as the Loading Handler

A lazy component is not immediately available when React encounters it — the chunk download is asynchronous. React uses Suspense to handle this: while the chunk loads, React suspends rendering of that subtree and shows the nearest Suspense fallback instead. When the chunk arrives, React resumes and renders the actual component. The fallback prop is any React node: a skeleton, a spinner, or even null.

Suspense can be placed at different levels. A single Suspense around all lazy routes shows one loading state for any route transition. Individual Suspense per route shows loading states independently. The granularity is a UX decision.

ErrorBoundary for Chunk Failures

If the chunk download fails (network error, CDN unavailable, cache invalidation after a deploy), the Suspense fallback disappears and an error is thrown. Without an ErrorBoundary, this propagates to the root and shows a blank screen. An ErrorBoundary wrapping the Suspense catches the chunk load error and can show a "Retry" button that resets state and re-attempts the load.


Key Code Explained

import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
import { ErrorBoundary } from 'react-error-boundary';

// Each of these becomes a separate chunk file
// The chunk is only downloaded when the route is first visited
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Analytics = lazy(() => import('./pages/Analytics'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));

function App() {
  return (
    <ErrorBoundary
      fallbackRender={({ error, resetErrorBoundary }) => (
        <div>
          <p>Failed to load page: {error.message}</p>
          <button onClick={resetErrorBoundary}>Retry</button>
        </div>
      )}
    >
      {/* Suspense handles the chunk download state */}
      <Suspense fallback={<PageSkeleton />}>
        <Routes>
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/analytics" element={<Analytics />} />
          <Route path="/admin" element={<AdminPanel />} />
        </Routes>
      </Suspense>
    </ErrorBoundary>
  );
}


// Adapting a named export for React.lazy
// React.lazy requires the dynamic import to resolve to a { default: Component }
// Named exports need a .then() wrapper
const RevenueChart = lazy(() =>
  import('./charts/revenue').then((module) => ({
    default: module.RevenueChart,  // wrap named export as default
  }))
);


// Conditional lazy loading: only load when triggered
function AnalyticsPage() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <button onClick={() => setShowChart(true)}>View Analytics</button>

      {showChart && (
        <Suspense fallback={<ChartSkeleton />}>
          {/* Chunk is only fetched when showChart becomes true */}
          <RevenueChart />
        </Suspense>
      )}
    </div>
  );
}


// Preloading: start the fetch before the user navigates
// Call the dynamic import() directly — it returns and caches the Promise
function NavBar() {
  const handleAdminHover = () => {
    // Trigger the chunk download on hover, not on click
    // The browser starts fetching; when the user clicks, it may already be cached
    import('./pages/AdminPanel');
  };

  return (
    <nav>
      <Link to="/admin" onMouseEnter={handleAdminHover}>
        Admin
      </Link>
    </nav>
  );
}

The preload pattern works because import() returns a Promise and the browser caches module fetches. Calling import('./pages/AdminPanel') on hover starts the network request. By the time the user clicks and React encounters the lazy component, the chunk may already be in the browser's module cache, eliminating the loading state entirely for fast connections.


Tradeoffs

StrategyInitial bundleFirst visit to routeUser experience
No code splittingLarge (all routes)InstantFast start is slow, navigation is instant
Lazy loading routesSmall (current route only)Brief loading stateFast start, small delay on first route visit
Lazy + preload on hoverSmallUsually instant (preloaded)Best of both

What Interviewers Actually Check

  • Whether you know Suspense is required and what happens without it (runtime error)
  • Whether you add an ErrorBoundary for chunk load failures
  • Whether you know the named export adaptation with .then()
  • Whether you know lazy loading is worth it for large components/routes but not tiny ones
  • Whether you know about preloading via triggering the import() early

Follow-Up Questions

  1. How does Next.js handle code splitting compared to manual React.lazy? What does dynamic() in Next.js add?
  2. How do you confirm that code splitting is working? What do you look for in the Network tab in DevTools?
  3. When would you choose to put a Suspense boundary at the route level versus inside a specific component?
  4. How does React 18's streaming SSR interact with Suspense for lazy-loaded components?
  5. You have a component with named exports used in 10 places. Is creating 10 .then() wrappers the right approach, or is there a better way to structure the module?

Common Candidate Mistakes

  • Not wrapping the lazy component in Suspense — React throws immediately with a clear error message, but candidates forget this step
  • Not adding an ErrorBoundary — users see a blank screen when the CDN is unavailable or the chunk URL changes after a deploy
  • Using lazy with a named export without the .then() adapter — the component renders as undefined
  • Lazy-loading tiny UI components (less than 5KB) where the network round-trip cost exceeds the savings
  • Not knowing about preloading, so users always see a loading flash on the first navigation

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you use React.lazy with a dynamic import() and wrap the result in Suspense?
  • Can you explain what Suspense renders while the chunk is downloading?
  • Can you add an ErrorBoundary and explain what it catches?
  • Can you adapt a named export for use with React.lazy?
  • Can you preload a chunk before the user navigates to trigger the download early?

Summary

React.lazy(() => import('./Component')) tells the bundler to split that component into a separate chunk file. The chunk is not included in the initial bundle and is fetched only when React first renders the lazy component. Wrap all lazy components in <Suspense fallback={...}> to show a loading UI while the chunk downloads — Suspense is required, not optional. Add an <ErrorBoundary> around the Suspense to catch chunk load failures and give users a retry option. React.lazy requires the dynamic import to resolve to a module with a default export; adapt named exports with a .then() wrapper. Preload chunks by calling the dynamic import() directly on hover or focus to eliminate the loading delay for highly likely navigation paths.

Related Videos
Watch these videos to learn more about this topic
Frequently Asked Questions

Can I use React.lazy for named exports?

Not directly. React.lazy requires the dynamic import to resolve to a module with a default export. Adapt named exports with a .then() wrapper: lazy(() => import("./charts").then(m => ({ default: m.RevenueChart })))

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement