Multiple Pages with React Router

In this article we're going to get started hooking up React Router, so we can get multiple pages going in our blog on both the Client and Server side.

We're going to start where we left off in the last article.
If you need to catch up:

git clone https://github.com/lawwantsin/webpack-course.git
cd webpack-course
git checkout react-router
npm install

React Router

React Router is a component based way to setup routing in our app. I'm using it here because they've been a vocal part of the Server side rendering and Dynamic importing conversation. It's also a great supplemental library for all your React projects.

In terminal:

npm i react-router react-router-dom

We need to make our blog better. Let's add 2 more pages to the blog. The gallery page, which will list our articles and the article page, which will render our individual posts. We'll leave what we have so far as an about page.

In AppRoot.js, let's add some page level components,

import React from 'react'
const MarkdownData = require('../../data/post.md')
const imagePath = require('../images/link.jpg')
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'

const About = () => (
  <div className="profile">
    <img src={imagePath} />
    <h1 id="title">{MarkdownData.title}</h1>
    <div
      className="content"
      dangerouslySetInnerHTML={{ __html: MarkdownData.__content }}
    />
  </div>
)

const Gallery = () => (
  <div>
    <h1>Gallery</h1>
  </div>
)

const Article = () => (
  <div>
    <h1>Article</h1>
  </div>
)

export default class extends React.Component {
  constructor(props) {
    super(props)
    this.state = {}
  }

  render() {
    return (
      <Router>
        <div>
          <div className="nav">
            <Link to="/">Gallery</Link>
            <Link to="/about">About</Link>
            <Link to="/article">Article</Link>
          </div>
          <Route exact path="/" component={Gallery} />
          <Route path="/about" component={About} />
          <Route path="/article" component={Article} />
        </div>
      </Router>
    )
  }
}

We moved the profile stuff to an About component. We then create 2 empty components, Gallery and Article, which we'll fill in later. We set up the root of our routes in the render function. Link and Route are paired up inside a Router component and that's it. As long as the to attributes match the path attributes you're good to go.

If we restart our server and in our browser we see an error having to do with our server side needing a StaticRouter.

Missing Image

We'll get to that in one minute, but first, let's disable server side rendering, just to see how easy it is to switch back and forth.

In webpack.dev-config.js. Let's uncomment these lines, or if you deleted them, write them back in.

const HTMLWebpackPlugin = require('html-webpack-plugin')

new HTMLWebpackPlugin({
  template: './src/index.ejs',
  inject: true,
  title: "Link's Journal",
})

And in src/server/main.js comment out the HotServerMiddleware:

// server.use(webpackHotServerMiddleware(compiler))

Okay, now we can rerun our server and, it's working.

Missing Image

Unstyled but working. At the top, let's import a new stylesheet to get our navigation looking better.

In AppRoot.js:

import './nav.css'

In src/components/nav.css let's update the look of our navigation:

.nav {
  width: 100%;
  line-height: 2em;
  background-color: black;
}

.nav a {
  color: white;
  text-decoration: none;
  padding: 0 20px;
}

And in main.css let's make the h1 style every page the same. Instead of .profile > h1.

h1 {
  font-size: 5em;
  font-family: sans-serif;
  color: white;
  text-shadow: 0 0 20px black;
  text-align: left;
}

That looks better. And it works. It switches between pages beautifully!

We Broke HMR

Now, you'll notice we've lost the Hot reloading on the CSS changes. That happened in the last episode when we switched out our style-loader for the ExtractTextPlugin so that main.css would be output. There's a great way to get it working again, but let's leave it as is for now and move onto Routing on the server.

Server-Side React Router

To get server side routing working, we need 2 things to happen. First, we want to use the same routes for both client and server. So let's take the routes out of AppRoot.js and place them in a new file. While we're at it let's do the same for our 3 components.

In terminal:

cd src/components
touch Routes.js About.js Gallery.js Article.js

Inside Routes.js let's copy over the react router code in AppRoot.js:

AppRoot.js should look like:

import React from 'react'
import { BrowserRouter } from 'react-router-dom'
const Routes = require('./Routes')
require('./nav.css')

export default class extends React.Component {
  constructor(props) {
    super(props)
    this.state = {}
  }

  render() {
    return (
      <BrowserRouter>
        <Routes />
      </BrowserRouter>
    )
  }
}

Routes.js should look like:

import React from 'react'
import { Route, Link } from 'react-router-dom'
import Gallery from './Gallery'
import About from './About'
import Article from './Article'

export default ({}) => (
  <div>
    <div className="nav">
      <Link to="/">Gallery</Link>
      <Link to="/about">About</Link>
      <Link to="/article">Article</Link>
    </div>
    <Route exact path="/" component={Gallery} />
    <Route path="/about" component={About} />
    <Route path="/article" component={Article} />
  </div>
)

In About.js:

import React from 'react'
const MarkdownData = require('../../data/post.md')
const imagePath = require('../images/link.jpg')

export default () => (
  <div className="profile">
    <img src={imagePath} />
    <h1 id="title">{MarkdownData.title}</h1>
    <div
      className="content"
      dangerouslySetInnerHTML={{ __html: MarkdownData.__content }}
    />
  </div>
)

In Gallery.js:

import React from 'react'

export default () => (
  <div>
    <h1>Gallery</h1>
  </div>
)

In Article.js:

import React from 'react'

export default () => (
  <div>
    <h1>Article</h1>
  </div>
)

Now we need to change our server to render using the StaticRouter. StaticRouter takes the same routes as the BrowserRouter on the client side, and provides a simple way to hookup SSR in Routing.

In render.js at the top.

const { StaticRouter } = require('react-router')

Inside the res.send():

<div id="react-root">
  $
  {ReactDOM.renderToString(
    <StaticRouter location={req.originalUrl} context={{}}>
      <Routes />
    </StaticRouter>
  )}
</div>

Notice we're getting out route from req.originalUrl and we're passing an empty object to context as that is required by StaticRouter, but not needed here. Cool. Now when we rerun it we see the routes reload and have real markup in the source. How does prod look?

npm run prod

Looks good too. Wow, we've build something real here. Now to get these pages come content. Right after we wrap up this section with the universal dynamic imports.

In Sum

In this article we hooked React Router into our client and server side rendering, making 2 new pages for our Blog and separating our pages into their own components. We also looked at unhooking the server side temporarily. If you need to look at the final code:

git checkout react-router-final

Up Next

Next we're going to finish off our Universal rendering build with the inclusion of Dynamic Imports. By the end of the next lesson we'll be serving individual Javascript and CSS bundles for each page in our Blog. See you there.