Isomorphic, SSR App Tutorial Made Simple: React.js, react-router, node.js with state

In my previous tutorial: https://codeburst.io/headache-free-ssr-react-js-node-js-and-es6-app-boilerplate-tutorial-267f7be0b7b5 , we created a very simple app that didn’t handle routes. For most (if not all apps) you will want to make your pages dynamic with your url. This tutorial we will introduce ‘react-router-dom’ and on the server it will initialise the app with data.

Typically when I want to create an app, I want to initialise it with a state, using data fetched from an api or database. So for this tutorial to be practical for you, I have created a couple of components, a service (an api call to get pokemon! I used http://pokeapi.co/api/v2/ability/telepathy) and props, so that you can see how it all comes together. You can easily amend to fit your project.

Some of what we will cover is a repeat of the other tutorial, but practice makes perfect!

To skip the intro (which is a rough repeat) click here

For this tutorial, I assume that you will have basic knowledge of react.js and that you can create a basic react app/components. Thus some of the components in this tutorial, I will not walk you through. I have also written tests (as I am a big advocate of TDD!) using jest, enzyme and supertest. You can view my original code with all my tests HERE. If you are feeling wild, maybe try and write a test for each file we create, as we go!

Lets set the scene

React.js will be our JavaScript library for our user interface (client side). We will create an express/node.js web server, which will render our app and react-router-dom which will allow our app to respond to url changes!

Wanting to keep up with the cool kids (and because it really has some fabulous new features!), we will use ES6 (a.k.a ES2015). ES6 is a major update to JavaScript that includes dozens of new features. However, not all browsers have kept up with the new features, so this is where babel gets involved. Babel is a JavaScript compiler, that will transpile our code written in ES6 to ES5, which can be read and understood by browsers.

I think it is safe to say, that browsers can be a wee bit old school! They also aren’t familiar with react’s jsx syntax. This hybrid html/js looking tag syntax, will also need to be transpiled using babel. However, as react apps typically have many modules and depencies, we will also be using webpack. Webpack allows us to bundle/compile all our assets (e.g. js files, images, css) into individual files e.g. js, css, png!

I found that having an outline of the folder structure is always helpful to make sure that I don’t get lost along the way.

Ready, Steady, Code…

$ mkdir ssr-react-router-node-app
$ cd ssr-react-router-node-app
$ npm init

Accept all the default values when you get the various prompts, by hitting enter.

Hello Client Side

Lets install our dependencies for our react app:

$ npm i --save react react-dom react-router-dom
  • react-dom is needed at the top level of our app i.e. in our client side entry file (See app/index.js where we use it, and webpack.config.js where we reference it as our entry point)
$ npm i --save-dev babel-core babel-cli babel-loader babel-preset-env babel-preset-react webpack
  • babel-cli is the terminal interface i.e. it allows us to compile files from the command line
  • babel-loader allows transpiling JavaScript files using Babel and webpack
  • babel-preset-react, does what it says on the tin(!): transpiles jsx to readable js.
  • babel-preset-env, transpiles es2015/es2016/es2017 (ES6 or ES7 or ES8!) to ES5. It is the equivalent of babel-preset-es2015, babel-preset-es2016, and babel-preset-es2017 together).
$ touch .babelrc .gitignore webpack.config.js

In the file .babelrc type:

## .babelrc
{
    "presets":[
        "env", "react"
    ]
}

This is the babel configuration file. We have “env” and “react” as we want babel to look our for their syntax and to transpile these.

We created .gitignore as I presumptively assumed that you have ‘git init’ this project and might push your code to a repository somewhere! If you have no intention to do this then feel free to delete this file. This file is used to declare any files or folders that you don’t want git to keep track of.

## .gitignore
build
node_modules
coverage // when I run my tests (jest), I also generate test coverage reports

Now on to our webpack.config.js file!

What is going on?

  • line 7 (entry): the entry point of our react app, where all other components and module dependencies stem from. (Remember when I mentioned react-dom earlier?). This is where webpack and babel will enter our app to transpile and compile the code.
  • line 8 (output): so the code has been transpiled and compiled, where shall we put this output? This says, in the root directory, have a build folder and place our ‘bundled’ js file in it, and lets call this file bundle.js. Note: In our package.json file, we will specify our app to run from the build folder not from the app folder! (As the browser needs the transpiled code!)
  • line 12 (module): this is where we can define and add the loaders that we want to use. (I plan to extend this to include css-loader/style-loader). line 15 (test), is a regex that says look for all files ending in .js.

Lets create some file structure:

$ mkdir app
$ touch app/index.js

We now have our entry point index.js file and can finally use react-dom!

WHAT IS HAPPENING HERE?

line 3: we get our BrowserRouter which is a react component (<BrowserRouter />) and we rename it to Router (that cool ES6 syntax!)

line 9: our preloaded state which we have called __PRELOADED_STATE__ (I have been creative with the name!) is passed into the app as a prop called pokemon. (we will define __PRELOADED_STATE__ on the server).

Note we could have replaced line 2:

import { render } from 'react-dom';
### equivalent to
import ReactDOM from 'react-dom';
const render = ReactDOM.render;

Create a components folder within the app folder. This is where we will create our highest order component - App and where I am going to create some Routes (in our App.js file):

You can see that I have defined 3 routes:

  • line 19: “/”: renders the Home component. I have used exact as otherwise all routes will match this path (as all routes contain localhost:8080/ !!!!)
  • line 20: “/pokemon” renders the react-router’s component <Redirect />, i.e. when hitting url localhost:8080/pokemon it will instantly redirect you to url localhost:8080/pokemon/ability/:ability . I have also used exact as otherwise the route localhost:8080/pokemon/ability/:ability would match this path (as that route contains localhost:8080/pokemon !!!!)
  • line 21: “pokemon/ability/:ability” renders the List component. Please note, I have only passed in location as a prop as want to get the route param i.e. :ability from the url

line 13: we get our pokemon prop that we defined in app/index.js


As I mentioned, I will not be going into components List and Home. You can see all the components tests here.


Server Side Rendering (SSR) Time

Now lets create our express/node server.

$ npm i --save express cors
$ npm i --save-dev nodemon
  • nodemon looks out for changes in your source code and on detection will automatically restart the server so that you can see those changes “instantly”! (pretty cool, right!). NOTE: For production we will be replacing nodemon with node.
  • cors stands for cross-origin resource sharing which allows for cross origin resource requests (you might not need this if you are using your own api in the same domain). This is a middleware.

Lets create our server folder and our index.js and app.js files:

$ mkdir app/server
$ touch app/server/app.js
$ touch app/server/index.js

What is going on in our app.js file?

  • line 9: __dirname refers to the root of where that file is being run. I will explain this more when we add scripts to our package.json.

express.static determines the root directory from which all static assets/files will be served.

(try console.log(__dirname) to see for yourself! __dirname should be something ending with ssr-react-router-node-app/build/server )

  • line 11: for all requests to the server use the middleware cors. For a better understanding of middleware (looking specifically with express) read http://expressjs.com/en/guide/using-middleware.html
  • line 12: this is saying for all requests to the server use the assets (that we have defined on line 9, which should equal (.*)/ssr-react-router-node-app/build)
  • line 14: for all get requests call function router

But what is this router module? (I hear you shout!) This will be where all the cool server side rendering happens!

Lets Get A Routing!

We have already in our App.js file defined 3 routes

/
/pokemon
/pokemon/ability/:ability

We need to let our server know that only these 3 routes should be recognised (if not we want to give a 404!)

So lets create a cheeky file called routes.js where we will define all the routes that we want.

app/server/routes.js

NOW lets get to our router.js file:

WOH!?! That looks crazy!!! Lets take a walk through this…

  • line 6: renderFullPage is our function that just creates our html.

line 11 is where the magic happens!

Create a renderFullPage.js file.

  • line 11, we have created and assigned our window.__PRELOADED_STATE__ . Note that we could have called this anything e.g. window.__I_LOVE_TO_CODE__ and then we would just have to update our app/index.js file! To minimise security risk, we use replace(/</g, ‘\\u003c’) to get rid of the script tags, to help prevent dangerous script injections. Line 13 is our bundle.js. Note that we have it as /bundle.js. This is because we defined all our static assets to be served from the build folder (as defined in our server/app.js file) and our bundle.js file sits in the build folder (build/bundle.js)

  • line 7: this is our service (that we created), which we will use to fetch our data later. Mine is called getPokemon! I used axios for my http requests.
$ npm i --save axios

Create a services folder where we will write our getPokemon.js file.

app/services/getPokemon.js

  • line 12: this is where we check whether the page url is a url we recognise i.e. does req.url match with any of our paths we defined in routes.js. NOTE: We did not have to use the matchPath function(imported on line 3), we could have created our own function to compare the req.url with a route that we defined in routes. If it does match, it returns an object (try console logging it!) otherwise it returns null. The return value is assigned to the constant match.
  • line 14: we ask, does match not exist? If yes (i.e. match equals null), send a 404 status and display ‘error. Note, a better practice would be to create a React component e.g. <UrlUnknown /> and render this. (You will know how to do this by the time this tutorial is over… Maybe give it try afterwards?). Line 16: you need to return as you don’t want to continue in the router function as you have sent what you needed!

line 19: LETS FETCH OUR DATA!!!!!!!

We call our service getPokemon.withAbility which returns a promise (hopefully a happy one)! If it is (happy) resolved then we can get the resp.data and manipulate it.

line 21: we have manipulated the data we got back (I just wanted the array of pokemon) and assigned it to a constant (pokemon), which will be assigned to our window.__PRELOADED_STATE__ (in server/renderFullPage.js) and then used by the client (as seen in our app/index.js file).

line 25: We can’t pass a react component to res.send(), as the browser won’t recognise it! So we must pass it as an html string. Say hello to renderToString! Thus we pass in our react App with our pokemon prop. You hopefully will have noticed that in our app/index.js file we wrapped our <App /> in BrowserRouter (although we renamed it to Router), but here we have wrapped our App in <StaticRouter>. That is because on the server it is stateless, so we need a stateless router i.e. StaticRouter! We have to pass StaticRouter 2 props:

  • context: which is always assigned as an empty object (line 23). It allows us to handle redirects. As our server is static, it cannot change the apps state, thus when there is redirect, context is updated such that context.action, context.url, context.location exists. Try console logging it! (For this simple tutorial I haven’t taken advantage of this feature)
  • location: where we always pass in req.url, so that the routes (server vs client) always match!

line 29: We pass in our html and our preloaded stated (which we have called pokemon) to the renderFullPage function that we created earlier and BOOOOOOM… we send that html!!!!!

line 31: if our api request to fetch data throws an error, we will catch it here and then send a 404 and our error message. (Again, you could render another component!)

OMG!!!!!! Have we finished????

NO!!!! WE NEED TO TRANSPILE OUR SERVER AS IT USES ES6!

We can either extend our webpack, so that our server side also gets transpiled and then bundled or we can just use babel. As typically my projects don’t expand the server side that significantly, i.e. the server doesn’t have that many modules, I don’t see much benefit to bundling them into one file. So we will just use babel to do this!

Update our package.json

In the scripts section lets write a command to transpile AND compile our “client side” code using webpack (by client side, I mean our app/index.js file and all modules imported within, which as mentioned earlier is the entry point to our app (remember react-dom!)). Lets call it “build:client”:

"scripts": {
"build:client": "webpack --config ./webpack.config.js/"

}

We also want to transpile our entire app as the server will be importing our react component <App /> (amongst other things!). Lets create a script called “build”:

"scripts": {
"build": "babel ./app -d build",
"build:client": "webpack --config ./webpack.config.js/"
}

We have told babel to get the app folder, transpile it and put that transpiled code into the build folder!

Hang on… nodemon will watch for changes in ./build/server/index.js, but we also need to tell babel and webpack to look out for changes so that they can re-transpile and/or compile! Lets create scripts “build:watch:client”and “build:watch”:

"scripts": {
"build": "babel ./app -d build",
"build:watch": "babel ./app -d build --watch",
"build:client": "webpack --config ./webpack.config.js/",
"build:watch:client": "webpack --config ./webpack.config.js/ --watch"
}

Frustratingly, when in — watch mode, we cannot run two commands in the same terminal shell, as it is so keenly watching the first command, it never reaches the 2nd command! So lets install a helpful little package called parallelshell, (Guess what it does!).

$ npm i --save-dev parallelshell

Lets create a script that will build both the server and client, and another script that builds AND runs our app all in dev mode using our own server. Lets call it “build:prod” and “start:dev”, respectively:

"scripts": {
"build": "babel ./app -d build",
"build:watch": "babel ./app -d build --watch",
"build:client": "webpack --config ./webpack.config.js/",
"build:watch:client": "webpack --config ./webpack.config.js/ --watch",
"build:prod": "npm run build && npm run build:client",
"start:dev": "parallelshell 'npm run build:watch' 'npm run build:watch:client' 'nodemon ./build/server/index.js'"
}

Now in your terminal run

$ npm run build:prod
$ npm run start:dev

WINNING DANCE!!!!!


To run in production, so that you don’t have any hot reloading, add the script “start”:

"scripts": {
"build": "babel ./app -d build",
"build:watch": "babel ./app -d build --watch",
"build:client": "webpack --config ./webpack.config.js/",
"build:watch:client": "webpack --config ./webpack.config.js/ --watch",
"build:prod": "npm run build && npm run build:client",
"start": "npm run build:prod && NODE_ENV=production node ./build/server/index.js",
"start:dev": "parallelshell 'npm run build:watch' 'npm run build:watch:client' 'nodemon ./build/server/index.js'",
}

You will have seen in my source code that I have also set up a webpack.prod.config.js file. Currently the only difference is that I have minimised the bundle. If you want to use this file in your production scripts, then just reference webpack.prod.config.js in your “build:client” script.

To run my tests using jest and to check coverage, I wrote the script:

"test": "jest --watch --coverage"