Multi-page website with React in 2017

A tutorial for getting a simple app running on Heroku to play around with React, Webpack, and Express

tl;dr

It’s still really kind of hard to set up the basics with React from scratch; follow this tutorial or clone this repo instead!

Background

I’ve been wanting to learn how to use React for a couple of months now. There’s a bunch of reasons why, but some of the most prominent for me:

  • We use it at Stripe for a few of our properties, and I like to be fluent in our tech stack even though I work on marketing
  • Technologies like Webpack 2 and babel are really enticing
  • There’s just a ton of buzz around it and I want to know why!

As I’ve tried to poke around and learn from the plethora of tutorials out there, I’ve discovered that the landscape for React and its periphery of tooling is constantly changing these days. This also means that it’s really kind of hard to just get to a basic setup to play around with React itself.

Personally, every time I ran npm init, I was faced with a thousand tutorials that didn’t quite work—either I was cloning git repos with out-of-date package versions, or they included a tool I don’t need, or they weren’t applying today’s best practices. And none of them show you how to deploy the app to someplace like Heroku and have everything, well, just work.

Sidenote: Why not create-react-app ? Well, it certainly gets the closest to what I wanted. However, it doesn’t allow for additional customization of webpack configuration or any SASS support without ejecting first, so onwards we trudge :(

So, after reading through many, many tutorials (and banging my head against my desk many times), I’ve made all the adjustments I wanted and finally have the scaffolding for my simple app set up.

I thought I’d document what it took to get here so that others in my shoes could get to the starting line a little faster.


The “stack”

This tutorial will help you:

  • create a multi-page React app that’s hosted for free on Heroku.
  • We’ll use React Router to serve up different things at different URLs.
  • We’ll also use Webpack (and set up support for babel and SASS).
  • We’ll wrap Webpack’s built in webpack-dev-server thinly with Express to actual serve our pages.

Notes

From reading many, many tutorials, this “stack” seems to include the most popular tooling for a good starting point to diving into React.

I’ve used best practices where I knew them, but this is unlikely to be a highly performant app since I’m still learning and finding my way around—I’ll update this blog as I learn more.

Please get in touch or create a pull request if anything’s amiss.


Prerequisites

This tutorial assumes familiarity with general web development concepts and JavaScript. More tactically, it assumes that you have installed:

  • Node (including npm):
$ node -v
v7.5.0
$ npm -v
v4.1.2
$ git --version
git version 2.11.1

File setup

Alright, let’s get started. I’m going to create a two-page website that has a “home” page and a “contact me” page.

$ mkdir -p about-me && cd about-me
$ npm init

You can either fill out the prompts when you npm init or just hit enter all the way through. You should now have a file called package.json in your folder.

about-me/
└── package.json

While we’re at it, let’s also create our git repo. We’ll also ensure we won’t push the thousands of files into our repo that are soon going to get populated by installing node packages:

$ git init
$ touch .gitignore && echo 'node_modules' > .gitignore

Now, let’s create the directory structure we’ll need:

$ touch Procfile && touch server.js && touch webpack.config.js && touch webpack.config.dev.js && mkdir -p src/components/views && mkdir -p src/stylesheets && touch index.html && touch src/index.jsx && touch src/routes.jsx && touch src/components/app.jsx && touch src/components/views/home.jsx && touch src/components/views/contact.jsx && touch src/stylesheets/base.scss && touch src/stylesheets/home.scss && touch src/stylesheets/contact.scss

Now our directory should look like:

about-me/
├── src/
| ├── components/
| | ├── app.jsx
| | └── views/
| | ├── home.jsx
| | └── contact.jsx
| ├── index.jsx
| ├── routes.jsx
| └── stylesheets/
| ├── base.scss
| ├── home.scss
| └── contact.scss
├── .gitignore
├── index.html
├── package.json
├── Procfile
├── server.js
├── webpack.config.js
└── webpack.dev.config.js

Open the folder with the editor of your choice—I use SublimeText. (With SublimeText, I recommend installing the plugins for JSX and SCSS format highlighting.) Here’s what the folder and file structure looks like right now:

Time to install some dependencies for the core React, plus serving and routing:

$ npm i --save react react-dom react-router
$ npm i --save express body-parser

Next, let’s get webpack installed with a few dependencies for transpiling scss and jsx into css and js:

$ npm i --save webpack webpack-dev-server webpack-dev-middleware webpack-hot-middleware
$ npm i --save extract-text-webpack-plugin@2.0.0-rc.3
$ npm i --save babel-core babel-loader babel-preset-es2015 babel-preset-react node-sass sass-loader css-loader style-loader

(Note: It’s important to install the non-default version of extract-text-plugin as it’s not compatible with the latest version of Webpack.)

Base & routing

Let’s get our index.html page set up to get to the compiled js and css:

<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="/public/app.css" />
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="/public/bundle.js" charset="utf-8"></script>
</body>
</html>

Routing

We use a module called react-router to get React to show different views for different URLs.

We’ll use index.jsx to set up our router and to ingest the right SCSS files…

import React from 'react';
import ReactDom from 'react-dom';
import { Router, browserHistory } from 'react-router';
import routes from './routes';
require('./stylesheets/base.scss');
require('./stylesheets/home.scss');
require('./stylesheets/contact.scss');
ReactDom.render(
<Router history={browserHistory} routes={routes} />,
document.querySelector('#app')
);

…and routes.jsx to actually define them:

import React from 'react';
import { Route, IndexRoute } from 'react-router';
import App from './components/app';
import Home from './components/views/home';
import Contact from './components/views/contact';
export default (
<Route path='/' component={App}>
<IndexRoute component={Home} />
<Route path='contact' component={Contact} />
<Route path='*' component={Home} />
</Route>
);

As you can see, the / path will get routed to app.jsx, and we’ll set up two views for / (home.jsx) and /contact (contact.jsx). We also specify that any URL not specified should serve up our home page.

Views and controllers

Now we can actually set up some of the simple controllers for the different pages we want to serve:

Here’s what app.jsx looks like:

import React, { Component } from "react";
export default class App extends Component {
render() {
return (
<div>
{this.props.children}
</div>
);
}
}

It does nothing but set up a scaffold to show the views we’ll set up next, starting with home.jsx:

import React, { Component } from "react";
import { browserHistory } from 'react-router';
export default class Home extends Component {
componentDidMount() {
browserHistory.push('/');
}
render() {
return (
<div id="home">
This is the home page.
</div>
);
}
}

Since this is the catch all view, I’ll elected to change the URL to / if someone hits this view. (e.g. If I actually request /shop, we’ll serve up the Home view and change the URL to /.)

Here’s the very similar contact.jsx:

import React, { Component } from "react";
export default class Contact extends Component {
render() {
return (
<div id="contact">
This is the contact me page.
</div>
);
}
}

Style

Let’s set up some dummy SASS files just to see everything checks out in the compilation. Starting with base.scss

$var: blue;
body {
background: $var;
}

…then, contact.scss

#contact {
color: #fff
}

…and finally, home.scss:

#home {
color: red;
}

These files are what we’ll eventually use to store our actual page-specific css styling.

Express and Webpack

We’re going to use express as a very thin wrapper around webpack when we deploy our app.

First up, let’s make a small change to package.json to let it know to use our version of Node as the engine and to change the entry point from index.js to server.js:

...
"main": "server.js",
"engines": {
"node": ">=7.5.0"
},
...

To run the app, we’ll need to set up two config files for webpack, which correspond to prod and dev environments.

webpack.config.js:

const ExtractTextPlugin = require('extract-text-webpack-plugin');
var webpack = require('webpack');
module.exports = {
context: __dirname,
entry: "./src/index.jsx",
output: {
path: __dirname + '/public',
filename: "bundle.js",
publicPath: '/public/'
},
module: {
loaders: [
{
test: /\.js|.jsx?$/,
exclude: /(node_modules)/,
loader: 'babel-loader',
query: {
presets: ['react', 'es2015']
}
},
{
test: /\.scss$/,
loader: ExtractTextPlugin.extract('css-loader!sass-loader')
}
]
},
resolve: {
extensions: ['.js', '.jsx'],
},
plugins: [
new ExtractTextPlugin({ filename: 'app.css', allChunks: true })
],
devServer: {
historyApiFallback: true,
contentBase: './'
}
};

webpack.config.dev.js

var path = require('path');
var webpack = require('webpack');
module.exports = {
context: __dirname,
entry: "./src/index.jsx",
output: {
path: path.resolve(__dirname, 'public/'),
filename: "bundle.js",
publicPath: '/public/'
},
module: {
loaders: [
{
test: /\.js|.jsx?$/,
exclude: /(node_modules)/,
loader: 'babel-loader',
query: {
presets: ['react', 'es2015']
}
},
{
test: /\.scss$/,
loader: 'style-loader!css-loader!sass-loader?sourceMap'
}
]
},
resolve: {
extensions: ['.js', '.jsx'],
},
plugins: [
new webpack.HotModuleReplacementPlugin()
],
devServer: {
historyApiFallback: true,
contentBase: './'
}
};

And finally, our server.js file looks like:

var path = require('path');
var bodyParser = require('body-parser');
var express = require('express');
var webpack = require('webpack');
var config = require('./webpack.config.dev.js');
var app = express();
var compiler = webpack(config);
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(require('webpack-dev-middleware')(compiler, {
noInfo: true,
publicPath: config.output.publicPath
}));
app.use(require('webpack-hot-middleware')(compiler));
app.use('/public', express.static('public'));
app.get('*', function(req, res) {
res.sendFile(path.resolve(__dirname, 'index.html'));
});
app.listen(process.env.PORT || 5000, function(err) {
if (err) {
console.log(err);
return;
}
console.log('Listening at http://localhost:5000');
});

Running our site

Let’s update our package.json file to add some scripts to make starting our servers easier:

...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack-dev-server --hot --inline",
"start": "node server.js"
},
...

With everything set up, you can now run:

npm run dev

(This will pick up our webpack.config.dev.js parameters.)

If you visit http://localhost:8080/ you should see:

If you want to run the site against our production setup, you can run:

npm start

If you visit http://localhost:5000/contact, you should see:

Deploying to Heroku

Commit

If everything looks good so far, let’s commit to git:

$ git add .
$ git commit -am "initial scaffolding for about-me app"

Setting up Heroku

I’m assuming you’ve created an account and followed their guide to get Heroku’s CLI set up.

First deploy

Inside the Procfile file, add the following:

web: node server

You can then follow the steps on Heroku’s documentation page to deploy your app:

$ heroku create
$ git push heroku master
$ heroku ps:scale web=1
$ heroku open

And that’s it! Our simple app is now running on Heroku!

Parting thoughts

This tutorial leaves you at just the beginning. You can now start making this simple app your own by customizing all the things and starting to experiment with the different parts of this particular React “stack”.

While it’ll be difficult for me to keep this tutorial up-to-date, I urge you to submit a pull request to the repo if you notice anything amiss!

Thanks for reading :)