Meteor + Webpack + React Router: A basic template with code splitting

I previously wrote an article on building a simple counter app using Meteor and Webpack, which can be found here. If you’re new to Webpack, you should start there. The next step to building a real app is integrating a router—in this case, React Router.

We also want to be able to take advantage of Webpack’s ability to lazily load our JavaScript, so we will go through a contrived example of a simple app with code splitting handled by React Router. For more information on how React Router handles this, read this how-to on Dynamic Routing.

You can see this in action in the first example below when 3.web.js and 2.web.js are loaded only when the relevant routes are activated. In the second example, you can see that nothing is loaded when you change routes because everything was loaded as soon as the page rendered. An example of an app without code splitting can be found here for your reference.

An example of an app with code splitting.

The app will have three routes: the root (/), settings (/settings), and other (/other). Each will have a simple component that will only load when the route is hit. Obviously if this were a real app, you wouldn’t need to split your entry points since each component is so small, but you can imagine a situation where loading everything up front could cause significant delays in load time.

An example of an app without code splitting.

You should first read the documentation for code splitting from Webpack, and a little multiple entry points. It might not make perfect sense after reading the docs, but that should at least give you a better sense of what we’re doing.

I would also advise starting by cloning this repo to save some time. If you don’t want to clone the repo, you can go through the steps in this article to build your own.

Step 1

Since we’re building this with React Router, we need to add it to webpack.packages.json. NPM 3+ no longer automatically adds the history package, which is a dependency of react-router, so be sure you add both packages or your app will break.

{
"react": "^0.14.2",
"react-dom": "^0.14.2",
"history": "^1.12.0",
"react-router": "^1.0.0-rc4",
  "babel-loader": "^5.3.2"
}

Step 2

Now we need to change our file structure a bit to make it easier to separate our components. This part is not strictly necessary, but it makes for better organization. Our current file structure looks like this (and if you don’t have tree-cli, I highly recommend it):

├── client
│ ├── entry.jsx
│ └── webpack.conf.js
├── server
│ ├── entry.jsx
│ └── webpack.conf.js
├── webpack.html
└── webpack.packages.json

We want to change it to look like the structure below, with our entry points and modules in separate folders, a webpack.conf.js file for shared code between the client and server, a router file for each of our modules, and .jsx files for each of our components.

├── entry
│ ├── client
│ │ ├── entry.jsx
│ │ ├── routes.jsx
│ │ └── webpack.conf.js
│ ├── server
│ │ ├── entry.jsx
│ │ └── webpack.conf.js
│ └── webpack.conf.js
├── modules
│ ├── Home
│ │ ├──components
│ │ │ └── Home.jsx
│ │ └── routes.jsx
│ ├── Settings
│ │ ├──components
│ │ │ └── Settings.jsx
│ │ └── routes.jsx
│ └── Other
│ ├──components
│ │ └── Other.jsx
│ └── routes.jsx
├── webpack.html
└── webpack.packages.json

Step 3

Now we can remove the resolve.extensions from our entry/client and entry/server configuration files and move it to the shared configuration file entry/webpack.conf.js.

var path = require('path');
module.exports = {
resolve: {
root: path.join(__dirname, '..', 'modules'),
extensions: ['', '.js', '.jsx', '.json']
}
};

You’ll notice we also added resolve.root, which will make it easier for us to find our modules later on. What this does for us is assume that our components live one directory back and within modules. This way, we don’t have to type out the full directory path each time.

Then delete the Counter component and the Meteor.startup function from entry.jsx, and add an import to the routes.jsx file. Your client/entry.jsx file should now look like this:

import ReactDOM from 'react-dom';
import React from 'react';
import "./routes";

Step 4

Now we need to create a bunch of components to render on different routes. Each component will simply display its name in an <h1> tag with a link to the other routes. For example, in Home.jsx:

import React, { Component } from 'react';
import { Link } from 'react-router';
export default class Home extends Component {
render() {
return (
<div>
<Link to="/">Home</Link><br/>
<Link to="other">Other</Link><br/>
<Link to="settings">Settings</Link>
<h1>This is the Home component.</h1>
</div>
)
}
}

We are using ES2015 syntax to export the component as a module that we can import later on. Do the same for the other two files with names changed accordingly.

Also, if you are new to React Router, Link from React Router is basically just an anchor tag (<a>) with a few special abilities.

Step 5

Now we need to export these modules so that our router knows what to do with them. In modules/Home/routes.jsx, add the following:

module.exports = {
getComponent(location, cb) {
require.ensure([], (require) => {
cb(null, require('./components/Home'))
})
}
}

What this code is doing is exporting a few properties that our router expects so that we can load each component asynchronously. Think of it this way: when the router hits a certain route, it’s going to request this component, which calls a function that returns the component you want to render. This will make more sense once we set up our router.

Go ahead and add the same code to Settings and Other, except with a small difference. We’re going to use the Home module as an IndexRoute (in previous versions of React Router, DefaultRoute) so we do not need to define a path. With Other and Settings, we need to define a path. Change both Other and Settings to look like the following, with proper names for each:

module.exports = {
path: 'other',
getComponent(location, cb) {
require.ensure([], (require) => {
cb(null, require('./components/Other'))
})
}
}

Step 6

The last piece is putting it all together in our router within /entry/client/routes.jsx. This part is a little bit more complicated.

First we need to handle our imports. The only one that might look unfamiliar is createBrowserHistory, which is a necessary piece of React Router.

import React from "react";
import ReactDOM from "react-dom";
import { Router, Route, Link, IndexRoute } from "react-router";
import createBrowserHistory from 'history/lib/createBrowserHistory';

Then we need to import our components:

import Home from "Home/routes";
import Settings from "Settings/routes";
import Other from "Other/routes"

And we’re going to add another higher-level component called App, which will live above all of our other components:

const App = React.createClass({
render() {
return <div>{this.props.children}</div>
}
})

Now comes the part that is probably a little different than you’re used to. We’re going to create a rootRoute variable that contains all of our routes. We will then pass this into our router.

const rootRoute = {
component: "div",
childRoutes: [{
path: "/",
component: App,
indexRoute: Home,
childRoutes: [
Settings,
Other
]
}]
}

In the code above, we define an outer component that is a <div> (if you change it to “span” and inspect element, you can see where this is located) and give it a child route.

This child route renders our App component, which has an index route of Home, which we want to render when the path is at the root (“/”). This also has two child routes for Settings and Other, which you recall we imported above. Keep in mind, we are not importing the components directly, but the route module defined above.

Then finally we render it to the DOM:

ReactDOM.render(
<Router history={createBrowserHistory()} routes={rootRoute} />,
document.getElementById("app")
)

If you do not include history, you will have some strange behavior in your URL involving hashes (#).

And that should do it!

Conclusion

Code splitting is not that hard, but it does take some effort. It’s overkill for most apps and you probably shouldn’t worry about it unless you have a compelling need to squeeze out a little bit of extra performance.


Sam Corcos is the lead developer and co-founder of Sightline Maps, the most intuitive platform for 3D printing topographical maps, as well as LearnPhoenix.io, an advanced tutorial site for building scaleable production apps with Phoenix and React.