Introducing React Loadable

Component-centric code splitting and loading in React

When you have a large enough application, a single large bundle with all of your code becomes a problem for startup time. You need to start breaking your app into separate bundles and load them dynamically when needed.

A single giant bundle vs. multiple smaller bundles

How to split a single bundle into multiple is a well solved problem with tools like Browserify and Webpack.

But now you need to find places in your application where you can decide to split off into another bundle and load it asynchronously. You also need a way to communicate in your app when something is loading.

Route-based splitting vs Component-based splitting

A common piece of advice you will see is to break your app into separate routes and load each one asynchronously. This seems to work well enough for most apps, clicking on a link and loading a new page is not a terrible experience.

But we can do better than that.

Using most routing tools for React, a route is simply a component. There's nothing particularly special about them. So what if we optimized around components instead of delegating that responsibility to routes? What would that buy us?

Route vs. component centric code splitting

It turns out quite a lot. There are many more places than just routes where you can pretty easily split apart your app. Modals, tabs, and many more UI components hide content until the user has done something to reveal it.

Not to mention all the places where you can defer loading content until higher priority content is finished loading. That component at the very bottom of your page which loads a bunch of libraries: Why does that need to be loaded at the same time as the content near the top?

You can still easily split on routes too since they are simply components. Just do whatever is best for your app.

But we need to make splitting up at the component-level as easy as splitting at the route-level. To split in a new place should be as easy as changing a few lines of app code and everything else is automatic.


Introducing React Loadable

React Loadable is a small library I wrote to make component-centric code splitting easier in React.

Loadable is a higher-order component (a function that creates a component) which makes it easy to split up bundles on a component level.

Let's imagine two components, one that imports and renders another.

import AnotherComponent from './another-component';

class MyComponent extends React.Component {
  render() {
    return <AnotherComponent/>;
  }
}

Right now we are depending on AnotherComponent being imported synchronously via import. We need a way to make it loaded asynchronously.

Using a dynamic import (a tc39 proposal currently at stage 3) we can modify our component to load AnotherComponent asynchronously.

class MyComponent extends React.Component {
  state = {
    AnotherComponent: null
  };

  componentWillMount() {
    import('./another-component').then(AnotherComponent => {
      this.setState({ AnotherComponent });
    });
  }

  render() {
    let {AnotherComponent} = this.state;
    if (!AnotherComponent) {
      return <div>Loading...</div>;
    } else {
      return <AnotherComponent/>;
    };
  }
}

However, this is a bunch of manual work, and it doesn't even handle a lot of different cases. What about when the import() fails? What about server-side rendering?

Instead you can use Loadable to abstract away the problem. Using Loadable is simple. All you need to do is pass in a function which loads your component and a "Loading" component to show while your component loads.

import Loadable from 'react-loadable';

function MyLoadingComponent() {
  return <div>Loading...</div>;
}

const LoadableAnotherComponent = Loadable({
  loader: () => import('./another-component'),
  LoadingComponent: MyLoadingComponent
});

class MyComponent extends React.Component {
  render() {
    return <LoadableAnotherComponent/>;
  }
}

But what if the component fails to load? We need to also have an error state.

In order to give you maximum control over what gets displayed when, the error will simply be passed to your LoadingComponent as a prop.

function MyLoadingComponent({ error }) {
  if (error) {
    return <div>Error!</div>;
  } else {
    return <div>Loading...</div>;
  }
}

Automatic code-splitting on import()

The great things about import() is that Webpack 2 can actually automatically split your code for you whenever you add a new one without any additional work.

This means that you can easily experiment with new code splitting points just by switching to import() and using React Loadable. Figure out what performs best on your app.

You can see an example project here. Or read the Webpack 2 docs (Note: some of the relevant docs are also in the require.ensure() section).

Avoiding Flash Of Loading Component

Sometimes components load really quickly (<200ms) and the loading screen only quickly flashes on the screen.

A number of user studies have proven that this causes users to perceive things taking longer than they really have. If you don't show anything, users perceive it as being faster.

So your loading component will also get a pastDelay prop which will only be true once the component has taken longer to load than a set delay.

export default function MyLoadingComponent({ error, pastDelay }) {
  if (error) {
    return <div>Error!</div>;
  } else if (pastDelay) {
    return <div>Loading...</div>;
  } else {
    return null;
  }
}

This delay defaults to 200ms but you can also customize the delay using a third argument to Loadable.

Loadable({
  loader: () => import('./another-component'),
  LoadingComponent: MyLoadingComponent,
  delay: 300
});

Preloading

As an optimization, you can also decide to preload a component before it gets rendered.

For example, if you need to load a new component when a button gets clicked you could start preloading the component when the user hovers over the button.

The component created by Loadable exposes a preload static method which does exactly this.

let LoadableMyComponent = Loadable({
  loader: () => import('./another-component'),
  LoadingComponent: MyLoadingComponent,
});

class MyComponent extends React.Component {
  state = { showComponent: false };

  onClick = () => {
    this.setState({ showComponent: true });
  };

  onMouseOver = () => {
    LoadableMyComponent.preload();
  };

  render() {
    return (
      <div>
        <button
          onClick={this.onClick}
          onMouseOver={this.onMouseOver}>
          Show loadable component
        </button>
        {this.state.showComponent && <LoadableMyComponent/>}
      </div>
    )
  }
}

Server-side rendering

Loader also supports server-side rendering through one final argument.

Passing the exact path to the module you are loading dynamically allows Loader to require() it synchronously when running on the server.

import path from 'path';

const LoadableAnotherComponent = Loadable({
  loader: () => import('./another-component'),
  LoadingComponent: MyLoadingComponent,
  delay: 200,
  serverSideRequirePath: path.join(__dirname, './another-component')
});

This means that your async-loaded code-splitted bundles can render synchronously server-side.

The problem then comes with picking back up on the client. We can render the application in full on the server-side but then on the client we need to load in bundles one at a time.

But what if we could figure out which bundles were needed as part of the server-side bundling process? Then we could ship those bundles to the client all at once and the client picks up in the exact state the server rendered.

You can actually get really close to this today.

Because we have the all the paths for server-side requires in Loadable, we can add a new flushServerSideRequires function that returns all the paths that ended up getting rendered server-side. Then using webpack --json we can match together the files with the bundles they ended up in ( You can see my code here).

The only remaining issue is to get Webpack playing nicely on the client. I'll be waiting for your message after I publish this Sean.

There's all sorts of cool shit we could build once this all integrates nicely. React Fiber will enable us to be even smarter about which bundles we want to ship immediately and which ones we want to defer until higher priority work is complete.


In closing, please install this shit and give me a star on the repo.

yarn add react-loadable
# or
npm install --save react-loadable