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
- 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. - 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.