lazy

Defers loading a component's code until it is first rendered, suspending while that code arrives.

Signature
const SomeComponent = lazy(load)
  • Added in React 16.6.0

Parameters

ParameterTypeDescription
$load() => Promise<{ default: ComponentType }>A function that returns a Promise or another thenable. React does not call load until the first time you attempt to render the returned component. After React first calls load it waits for the promise to resolve, then renders the resolved value's .default as a React component. Both the promise and its resolved value are cached, so React never calls load more than once.

Return value

React componentA React component you can render in your tree. While the code for the lazy component is still loading, attempting to render it will suspend — use <Suspense> to display a loading indicator while it loads.

Errors

If the promise returned by `load` rejects, React throws the rejection reason for the nearest Error Boundary to handle.

Changelog

  • PHP 18.0.0Works during server rendering with the streaming APIs (`renderToPipeableStream` / `renderToReadableStream`), which support Suspense on the server.
  • PHP 16.6.0Introduced together with `<Suspense>` for client-side code splitting.

lazy turns a promise of a module into a component. React does not call your loader until something actually renders that component; while the promise is pending the component suspends, and the nearest <Suspense> boundary shows its fallback.

When to reach for it

Use it where a large chunk of code is not needed on first paint: a route the user may never visit, a rich text or chart library behind a tab, a heavy modal, an admin panel most users never open. The win is a smaller initial bundle, so the payoff scales with how big the deferred module is and how rarely it is opened.

The two rules

First, the resolved value must have a default export that is a component. A module with only named exports needs a wrapper: import('./Chart').then(m => ({ default: m.Chart })). Second, call lazy at module scope. Calling it inside a component body creates a new component type on every render, so React unmounts the old tree and remounts a fresh one — the state resets and the loader appears to fire repeatedly.

Failure and fallbacks

A rejected promise (a chunk that 404s after a deploy, a dropped connection) throws to the nearest error boundary, not to Suspense. Ship both: a Suspense boundary for the pending state and an error boundary with a retry for the failure.

Example

Try it yourself
Loading editor…
Loading React…

When to use it

  • Split a route's component out of the main bundle so the landing page ships less JavaScript.
  • Load a chart or markdown-editor library only when the user opens the tab that uses it.
  • Defer an admin-only settings panel that most signed-in users never open.

Frequently asked questions

Why does my lazy component's state reset?

Because lazy() is being called during render instead of at module scope. Each call produces a different component type, so React treats it as a different component, unmounts the previous one and mounts a fresh copy — losing all its state. Move the const declaration outside the component.

Can I use lazy without Suspense?

No. Rendering a lazy component before its code has loaded suspends, and a suspending component with no <Suspense> boundary above it throws an error. The boundary can be anywhere above the lazy component, so a single one at the route level often covers many lazy children.

How do I lazy-load a component that is a named export?

Map the module to a default in the loader: lazy(() => import('./charts').then(m => ({ default: m.Chart }))). React renders whatever the promise resolves to under the .default key, so any thenable resolving to that shape works — including one you construct by hand.

More examples

Real code splitting, named exports, and an error boundary

Each tab's code is fetched the first time that tab is opened and never again, because React caches both the loader promise and its resolved value. The Suspense boundary covers the pending state; the class error boundary covers the other outcome — a chunk request that fails, which is common right after a deploy replaces the hashed filenames.

Example · jsx
import { Component, lazy, Suspense, useState } from "react";

// Default export: the plain form.
const Dashboard = lazy(() => import("./routes/Dashboard.jsx"));

// Named export: unwrap it into `default` yourself.
const Chart = lazy(() =>
  import("./charts/index.js").then(module => ({ default: module.Chart }))
);

class ChunkErrorBoundary extends Component {
  state = { error: null };

  static getDerivedStateFromError(error) {
    return { error };
  }

  render() {
    if (this.state.error) {
      return (
        <p>
          Could not load this section.{" "}
          <button onClick={() => window.location.reload()}>Reload</button>
        </p>
      );
    }
    return this.props.children;
  }
}

export default function App() {
  const [tab, setTab] = useState("dashboard");

  return (
    <div>
      <button onClick={() => setTab("dashboard")}>Dashboard</button>
      <button onClick={() => setTab("chart")}>Chart</button>
      <ChunkErrorBoundary>
        <Suspense fallback={<p>Loading section…</p>}>
          {tab === "dashboard" ? <Dashboard /> : <Chart />}
        </Suspense>
      </ChunkErrorBoundary>
    </div>
  );
}

Preloading before the user clicks

Hovering or focusing the button starts the same dynamic import that lazy will later use. Because the module registry dedupes the request and React caches the loader result, the chunk is usually already in memory by the time the click lands, so the fallback never appears. Keep the boundary anyway for slow connections.

Example · jsx
import { lazy, Suspense, useState } from "react";

const loadEditor = () => import("./Editor.jsx");
const Editor = lazy(loadEditor);

export default function App() {
  const [open, setOpen] = useState(false);

  return (
    <div>
      <button
        onMouseEnter={loadEditor}
        onFocus={loadEditor}
        onClick={() => setOpen(true)}
      >
        Open editor
      </button>
      <Suspense fallback={<p>Loading editor…</p>}>
        {open ? <Editor /> : null}
      </Suspense>
    </div>
  );
}

Discussion

  • Be the first to comment on this lesson.