react-loadable

A higher-order component for lazy-loading React components with dynamic imports, delay/timeout states, and server-side rendering support.

Library
npm
v5.5.0
16,504stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
41/100Fair
Development Activity0
Maintenance0
Community64
Maturity60
Momentum40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
71/100Good
Architecture68
Code Quality62
Innovation70
Learning Curve85

react-loadable is a small higher-order component that makes component-level code splitting easy in React apps. Instead of importing a component synchronously, you wrap it in Loadable({ loader: () => import('./Component'), loading: Loading }), and the library handles the loading state, error state, and eventual render for you — including delay thresholds before showing a spinner and timeouts for slow loads.

It was built at a time when route-based code splitting was the common advice, but the author argued that components — not just routes — are the natural unit for splitting a bundle: modals, tabs, and below-the-fold widgets can all be deferred independently. The library integrates with Webpack’s import() code splitting out of the box and ships a companion Webpack plugin (react-loadable/webpack) that emits a bundle manifest so server-rendered apps can know exactly which chunks were used per request and preload them correctly on the client.

At its peak it was one of the most widely adopted code-splitting solutions in the React ecosystem, used by companies like Atlassian, Cloudflare, Tinder, and the BBC, and it directly influenced the design of React.lazy/Suspense that later shipped in React core. Development has been dormant since 2018, and the README itself now points newer projects toward React’s built-in lazy() and Suspense, but the package remains widely installed in legacy codebases that predate those APIs.

What You Get

  • Loadable(opts) — wraps a loader function and a loading component into a component that handles async loading, error, and render states automatically
  • Loadable.Map(opts) — loads multiple modules in parallel via a loader map and renders once all have resolved
  • Delay and timeout options so the loading UI only appears after a threshold and can flip to a timed-out state for slow connections
  • A retry() method passed to the loading component so users can manually retry a failed dynamic import
  • Loadable.preloadAll() / Loadable.preloadReady() for eagerly resolving all registered loadable components, used to make server-rendered markup hydrate without a loading flash
  • A Webpack plugin (react-loadable/webpack) that emits a JSON manifest mapping modules to their output bundles, so a server can know exactly which chunks a given render touched

Common Use Cases

  • Splitting a large single-page app into per-route bundles so users only download the code for the page they’re on
  • Deferring component-level chunks like modals, tabs, or below-the-fold widgets that most users never interact with
  • Server-rendering a code-split app and preloading, on the client, exactly the chunks that were used to render the initial HTML
  • Reducing an app’s initial bundle size in legacy React (pre-16.6/pre-Suspense) codebases where React.lazy isn’t available
  • Building a shared loading/error/timeout UX pattern across many dynamically-imported components in one config object

Under The Hood

Architecture The entire runtime lives in a single module, src/index.js, built around one factory function, createLoadableComponent(loadFn, options), that both Loadable and Loadable.Map are thin wrappers over — Loadable calls it with a single-promise load() strategy and Loadable.Map with a parallel loadMap() strategy, so the entire loading/error/delay/timeout state machine is shared code rather than duplicated per entry point. State for an in-flight import is held in a closure variable (res) outside of React state, and only mirrored into this.state on resolution, which keeps re-renders minimal but means the loading result is a module-level singleton per Loadable() call rather than per-component-instance. A separate Capture component uses React’s legacy context API (contextTypes/childContextTypes) to let nested loadable components report which webpack module IDs they touched, feeding a parallel Webpack-side manifest built in src/webpack.js. Because every consumer flows through the same factory, a change to that one function’s state machine would ripple through both the plain and map-based loading paths simultaneously.

Tech Stack The source is plain CommonJS compiled with Babel 7 (@babel/preset-env, @babel/preset-react, plus class-properties and transform-async-to-generator plugins) into a published lib/ directory via a build script (babel src -d lib), with prepare re-running that build on install for consumers pulling from git. The only runtime dependency is prop-types, with React declared as a wildcard peer dependency; the devDependencies pull in Webpack 4 and Express purely to run the bundled SSR example server, plus flow-bin for the .flowconfig-driven type checking used sparingly across the source. Nothing beyond Babel and Webpack is required to consume the package — it ships pre-compiled, so end users don’t need Flow or any custom toolchain.

Code Quality Tests live in __tests__/test.js under Jest, exercising the loading/error/timeout state machine with react-test-renderer and snapshot assertions against fixture components in __fixtures__/. The component itself relies on React’s legacy lifecycle API (componentWillMount) and the legacy context API for Capture, both deprecated well before the library’s last commits — a reasonable choice at the time of writing (2017-2018) but one that surfaces deprecation warnings on modern React versions. There’s no TypeScript, no visible lint configuration, and CI is limited to a .travis.yml; test coverage of the core loading paths is present but narrow rather than extensive.

API Design The public surface is deliberately tiny — a single Loadable({ loader, loading, delay, timeout }) call replaces what would otherwise be several lines of manual import() handling, and the loading component receives a consistent props contract (error, pastDelay, timedOut, retry) regardless of which loading strategy is used underneath. This uniformity across Loadable and Loadable.Map is the library’s main ergonomic strength, and its design directly foreshadowed React’s later built-in React.lazy/Suspense APIs. The tradeoff is that SSR support requires wiring up a separate Webpack plugin and manually matching a bundle manifest to rendered modules — extra ceremony that React.lazy has since made unnecessary for most apps.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search