Async Components in React Router 4

When I initially updated this website from using React Router 3 to 4, and from Webpack 1 to Webpack 2, I had to abandon the simple setup I had for asynchronous routes using require.ensure due to changes between the new versions. At the time, I decided to just remove the async loading of components, but I added them back this week, and it was really easy.

Async Wrapper Component

I came across this code snippet, and with only making a couple changes, I was able to integrate it into my application. Essentially, you wrap the components you want to load async-ly with a higher-order component that manages the loading of the component with a function you provide. The function provided must return a promise that will then return the component import. Once the component has been requested, and then imported, it renders like normal.

async-wrapper.jsx

import React from 'react';

export function asyncWrapper(getComponent, name = undefined) {
  return class AsyncComponent extends React.Component {
    static Component = null;

    state = { Component: AsyncComponent.Component };

    componentWillMount() {
      // Has it not been loaded before?
      if (!this.state.Component) {
        // Call our import function.
        getComponent().then((Component) => {
          // If name provided --> get it. If not, get the default.
          AsyncComponent.Component = name ? Component[name] : Component.default;

          // Save the component.
          this.setState({ Component: AsyncComponent.Component });
        });
      }
    }

    render() {
      const { Component } = this.state;
      if (Component) {
        return <Component {...this.props} />;
      }

      return null;
    }
  };
}

Implement

Wherever your routes are defined, you'll just need to change how your components are imported, and Webpack will take care of the rest during your build. Below is a simplified version of the how the routes are setup for this site.

import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { asyncWrapper } from 'util/async-wrapper';

const Home = asyncWrapper(() => import('components/home'));
const About = asyncWrapper(() => import('components/about'));
const Blog = asyncWrapper(() => import('containers/blog'));

const App = () => {
  return (
    <div className="appComponent">
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
        <Route exact path="/blog" component={Blog} />
      </Switch>
    </div>
  );
};

export default App;

All the components here are the default export of their module. If you had a named export, you would supply a name as the second parameter to asyncWrapper(). Just as an example, here's what that would look like for the Home component.

const Home = asyncWrapper(() => import('components/home'), 'Home');

Gotchas

  1. It's important to note that if your async-ly wrapped components import other components, they will also need to be wrapped with asyncWrapper, or else they will be included in the main application bundle.
  2. If you deploy all your bundles at the root of your folder structure like I do — don't forget to include output.publicPath = "/" in your Webpack config file. Otherwise, your nested routes will attempt to retrieve a bundle using the wrong (nested) path.

By wrapping my components with asyncWrapper, I was able to decrease the initial JavaScript load size of the home page to approximately 25% of what is was before (excluding the vendor bundle). For small applications like this one, it's probably a very small real-world difference for most users, but I think it's good habit to be mindful of your visitors bandwidth.


;