React.js Apps with Pages


If you’ve ever made a Single Page Application with a JavaScript framework, chances are you used routing. Routing lets you pretend that your application has “pages”. Users can go to yourdomain.com/about and get a “page” that shows information about the company.

I’m putting page in double-quotes here, because in reality this “page” isn’t a real page.

With most popular build tool configurations, all scripts get mushed together into one massive .js file. When your user visits the yourdomain/about “page”, they’ll get the content for the homepage, your blog, your secret corner and all the other “pages” your application has. They might not even look at any of those, but they still have to download the content. This gets worse and worse the bigger your application gets.

With normal, non-framework webpages, when you open a new page, you download a new .html file. You get the new content, take your styles and scripts from the cache and you have your page! The benefit is (hopefully) obvious: Users get the content they want, nothing more and nothing less.

Can we somehow make something similar happen for a React.js application?

Prerequisites

react-router

This is our example setup with standard react-router routes:

var HomePage = require('./HomePage.jsx');
var AboutPage = require('./AboutPage.jsx');
var FAQPage = require('./FAQPage.jsx');

<Router history={history}>
  <Route path="/" component={HomePage} />
  <Route path="/about" component={AboutPage} />
  <Route path="/faq" component={FAQPage} />
</Router>

Note: A lot of code omitted for explanation purposes

If a users visits yourdomain.com/about, they’ll see the AboutPage component, if they visit yourdomain.com/faq they’ll see the FAQPage component and so on. When you build the app with this routing setup, those components will all be together in one single .js file with the rest of the code.

Thankfully, react-router lets us asynchronously specify components of <Route>s with a prop called getComponent. It takes a function with a location and a callback argument. react-router will only render ourComponent when we run callback(null, ourComponent).

getComponent

Lets rework our example above to support asynchronous components:

<Router history={history}>
  <Route
    path="/"
    getComponent={(location, callback) => {
      // Do async things here
      callback(null, HomePage);
    }}
  />
  <Route
    path="/about"
    getComponent={(location, callback) => {
      // Do async things here
      callback(null, AboutPage);
    }}
  />
  <Route
    path="/faq"
    getComponent={(location, callback) => {
      // Do async things here
      callback(null, FAQPage);
    }}
  />
</Router>

These components are now loaded asynchronously when needed. The components will still be in the same file and the application won’t look or feel differently, but without this we could never make pages work!

webpack

webpack has a feature called chunking, which means it outputs multiple files (“chunks”) instead of one big one. It determines which modules go into which files based on split points in your code.

Split Points

Webpack gives us different ways to define split points, but the most usable one for our purposes is require.ensure. An example require.ensure might look something like this:

function loadModule() {
  require.ensure([], function(require) {
    var module = require('module.js');
  }, "MyModule");
}

The module exported in module.js will be in a second file generated by webpack, and loaded as soon as the require.ensure is called in the browser. (This won’t happen until our loadModule function gets called)

Note: The third argument of require.ensure is the name of the module. This is optional, and if you don’t specify a name it will be assigned an ID.

It doesn’t work yet though, we also need to adapt our configuration to support chunking.

Configuration

In your webpack config file extend the output option with the chunkFilename:

output: {
  chunkFilename: '[name].chunk.js'
}

You also have access to the [chunkhash] and the [id] variables in the chunkFilename. The [name] variable falls back to the ID if no name is specified, so its often enough

This will work, but there’s a problem. Common dependencies will be in every single module. If you use React in your module and in your main application, both chunks will include the code for React.

We can prevent this with webpacks built-in CommonsChunkPlugin. In your config, add (or extend) the plugins option:

plugins: [
  new webpack.optimize.CommonsChunkPlugin('common.js')
]

Note: I like the name common.js because it makes it clear what the file contains, but the name is arbitrary.

Nice, code splitting works now! Lets combine that with react-router.

Putting it together

Remember our Routes with asynchronous components from above? Instead of requireing the HomePage component above the Router, lets move that to the getComponent function and use require.ensure to only download the component (which is in its own file) when it’s needed:

<Route
  path="/"
  getComponent={(location, callback) => {
    require.ensure([], function (require) {
      var HomePage = require('./HomePage.jsx');
      callback(null, HomePage);
    }, 'HomePage');
  }}
/>

This works, to make it even terser require the HomePage inside the callback:

<Route
  path="/"
  getComponent={(location, callback) => {
    require.ensure([], function (require) {
      callback(null, require('./HomePage.jsx'));
    }, 'HomePage');
  }}
/>

The full example from above with pages:

<Router history={history}>
  <Route
    path="/"
    getComponent={(location, callback) => {
      require.ensure([], function (require) {
        callback(null, require('./HomePage.jsx'));
      });
    }}
  />
  <Route
    path="/about"
    getComponent={(location, callback) => {
      require.ensure([], function (require) {
        callback(null, require('./AboutPage.jsx'));
      });
    }}
  />
  <Route
    path="/faq"
    getComponent={(location, callback) => {
      require.ensure([], function (require) {
        callback(null, require('./FAQPage.jsx'));
      });
    }}
  />
</Router>

This is how you add real pages to your React application!

Thanks to Ryan Florence for slight corrections of an earlier draft of this post


Liked this post? Sign up for the weekly newsletter!

Be the first to know when a new article is posted and get an inside scoop on the most interesting news and the freshest links of the week. (I hate spam just as much as you do, so no spam, ever. Promise.)


npm scripts PostCSS: Die bessere Alternative zu Sass und Less?