How to Create a Lazy-Loaded Component in React
Advertisement
🧩 Scenario
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
| Strategy | Initial bundle | First visit to route | User experience |
|---|---|---|---|
| No code splitting | Large (all routes) | Instant | Fast start is slow, navigation is instant |
| Lazy loading routes | Small (current route only) | Brief loading state | Fast start, small delay on first route visit |
| Lazy + preload on hover | Small | Usually instant (preloaded) | Best of both |
What Interviewers Actually Check
- Whether you know
Suspenseis required and what happens without it (runtime error) - Whether you add an
ErrorBoundaryfor 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
- How does Next.js handle code splitting compared to manual
React.lazy? What doesdynamic()in Next.js add? - How do you confirm that code splitting is working? What do you look for in the Network tab in DevTools?
- When would you choose to put a
Suspenseboundary at the route level versus inside a specific component? - How does React 18's streaming SSR interact with
Suspensefor lazy-loaded components? - 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
lazywith a named export without the.then()adapter — the component renders asundefined - 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.lazywith a dynamicimport()and wrap the result inSuspense? - Can you explain what
Suspenserenders while the chunk is downloading? - Can you add an
ErrorBoundaryand 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.
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