React.js Apps with Pages
January 18th, 2016
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 as a routing solution, because its the most complete and feature-rich routing solution for React
-
webpack as a build tool, for its superior chunking ability and general power
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 thechunkFilename
. 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 Route
s with asynchronous components from above? Instead of require
ing 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.)
Tweet this article