Back to React
React
medium
mid

How would you lazy load components with React.lazy and Suspense inside protected routes?

`React.lazy(() => import('./Page'))` defers the chunk; wrap with `<Suspense fallback={<Skeleton />}>`. Compose with route protection: check auth first, then lazy-render the protected component. Use `<ErrorBoundary>` to handle chunk-load failures (network blip). Prefetch on link hover to mask first-visit latency.

4 min read·~12 min to think through

Basic shape

tsx
const Dashboard = React.lazy(() => import("./Dashboard"));

function App() {
  return (
    <Suspense fallback={<Skeleton />}>
      <Dashboard />
    </Suspense>
  );
}

React.lazy returns a component that, on first render, triggers the dynamic import; Suspense shows the fallback while the chunk loads.

Inside a protected route

tsx
const AdminPanel = React.lazy(() => import("./AdminPanel"));

function ProtectedRoute({ role, children }: { role: string; children: ReactNode }) {
  const { user } = useAuth();
  if (!user) return <Navigate to="/login" replace />;
  if (!user.roles.includes(role)) return <Forbidden />;
  return <Suspense fallback={<Skeleton />}>{children}</Suspense>;
}

<Route path="/admin" element={<ProtectedRoute role="admin"><AdminPanel /></ProtectedRoute>} />

The auth check runs before the lazy chunk is requested — denies unauthorized users without downloading the admin bundle.

Handle chunk-load failures

A flaky network or a stale deploy can fail the dynamic import. Wrap with an error boundary:

tsx
class ChunkBoundary extends React.Component {
  state = { error: null };
  static getDerivedStateFromError(error) { return { error }; }
  componentDidCatch(err) {
    if (err.name === "ChunkLoadError") {
      // Deploy mismatch — full reload picks up the new manifest
      window.location.reload();
    }
  }
  render() { return this.state.error ? <Fallback /> : this.props.children; }
}

ChunkLoadError almost always means "user has stale HTML referencing chunks from a previous deploy" — reloading fixes it.

Prefetch on hover

tsx
function LinkPrefetch({ to, importer, children }) {
  const onHover = useCallback(() => importer(), [importer]);
  return <Link to={to} onMouseEnter={onHover} onFocus={onHover}>{children}</Link>;
}
<LinkPrefetch to="/admin" importer={() => import("./AdminPanel")}>Admin</LinkPrefetch>

By the time the user clicks, the chunk is in the browser cache. Next.js <Link> does this automatically.

Nested Suspense

You can nest Suspense boundaries so different chunks resolve at different times:

tsx
<Suspense fallback={<Skeleton />}>
  <Sidebar />
  <Suspense fallback={<MainSkeleton />}>
    <Dashboard />
  </Suspense>
</Suspense>

Sidebar renders as soon as its chunk + data are ready; Dashboard streams in next.

When NOT to lazy load

  • Above-the-fold content that's needed for first paint.
  • The login page itself (otherwise users see blank → skeleton → login).
  • Small components — chunk overhead outweighs benefit.

Server-side considerations

In Next.js App Router, dynamic imports work in both server and client components. next/dynamic adds { ssr: false } for client-only modules.

Combining with role-based code splitting

For role-gated features, don't ship the chunk at all to unauthorized users. Achieve this by:

  • Lazy import only inside the protected branch (above pattern).
  • Or split bundles per role at the server, only serving the bundle the user has access to.

Interview framing

"React.lazy(() => import(...)) for the chunk, wrap with <Suspense fallback={...}> for the loading UI. For protected routes, do the auth/role check before rendering the lazy component — unauthorized users don't trigger the download. Wrap with an error boundary that handles ChunkLoadError by reloading (almost always means stale HTML after a deploy). Prefetch on hover via onMouseEnter={() => import(...)} to mask first-visit latency — Next.js <Link> does this for free. Nest Suspense to stream different sections. Don't lazy-load the login page or above-the-fold content."

Follow-up questions

  • Why prefetch on hover?
  • How would you handle a chunk that fails to load?
  • When is lazy loading the wrong choice?

Common mistakes

  • Lazy-loading above-the-fold content.
  • No error boundary around lazy components.
  • Shipping the chunk regardless of auth.

Performance considerations

  • Per-route splitting wins LCP; prefetch hides cold visit latency.

Edge cases

  • Stale-deploy ChunkLoadError.
  • Suspense fallback flash on fast networks.
  • SSR with client-only lazy components.

Real-world examples

  • React Router lazy loaders, Next.js dynamic, Remix route splitting.

Senior engineer discussion

Seniors gate the import behind auth, handle ChunkLoadError, and prefetch judiciously.

Related questions