Headache free, Isomorphic, Boilerplate App Tutorial: React.js, node.js, SSR and ES6

August 03, 2017 0 Comments

Headache free, Isomorphic, Boilerplate App Tutorial: React.js, node.js, SSR and ES6

 

 

Having made the switch from Finance to Software Engineering in 2016 and becoming confident creating node/express servers and react apps, only connected via api’s, I decided to tackle server side rendering (SSR). I read many tutorials on how to create a “basic” react.js and node.js app, with server side rendering using ES6. After hours (dare I say days!?!) of trying to find a tutorial that didn’t assume knowledge and thus didn’t miss out crucial steps, to tutorials that created an app that were much more complicated and intricate than it needed to be, I decided to create my own, stress free tutorial!

For this tutorial, I assume that you will have basic knowledge of react.js.

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 index.html file.

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 a link to the final code (here you go) and an outline of the folder structure is always helpful to make sure that I don’t get lost along the way.

Within folder ssr-react-node-app
$ mkdir ssr-react-node-app
$ cd ssr-react-node-app
$ npm init

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

Lets install our dependencies for our react app:

$ npm i --save react react-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 install --save-dev babel-core babel-cli babel-loader babel-preset-es2015 babel-preset-react webpack html-webpack-plugin
  • 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-es2015 and babel-preset-react, do what they say on the tin(!): transpiles es2015 (ES6) to ES5 and jsx to readable js, respectively.
  • html-webpack-plugin allows webpack to use an html file that we have created, make a copy and then insert the script that refers to the bundled (compiled) js file that has just been created.
$ touch .babelrc .gitignore webpack.config.js

In the file .babelrc type:

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

This is the babel configuration file. We have “es2015” 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
nodemodules

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.
  • line 25 (plugins): tells webpack where to find the html template that we want to use and what to call that file.

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!

(Look at that cool ES6 syntax!!)

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 (in our App.js file):

Now, lets create our very important html document. I like to put this index.html file into a folder called public that sits within the app folder.

Exciting stuff! We now have our client side set up using webpack and babel! BUT hold on… lets create some script commands which will help us check out our cool app.

In development we like to have hot-reloading, where we can (almost) instantly see any changes that we have made. So lets install webpack-dev-server.

$ npm i --save-dev webpack-dev-server

webpack-dev-server creates an express server that renders the app. NOTE: When it bundles our app, it stores it in memory, thus you will not see the build folder with our bundle.js and index.html files.

In our package.json file we will create a script:

"scripts": {
"start:dev:client": "webpack-dev-server"
}

lets check out our app:

$ npm run start:dev:client

Open your browser and go to localhost:8080 and see the beautiful words: ‘Your React Node app is set up!’. If you change anything in your component/App.js file and then refresh the page, you should now see those changes!

webpack-dev-server is great, but we want to create and use our own express/node server. So going forward we will no longer use webpack-dev-server, instead, we will use express and (in dev mode) nodemon. Lets install them:

$ npm i --save express
$ npm i --save-dev nodemon
  • nodemon looks our 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.

Within our app folder, lets create a folder called server and within that our app.js file:

What is going on?

  • line 6: notice we have used dirname again (previously used in our webpack.config.js file). _dirname refers to the root of where that file is being run. I will explain this more when we add another script 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-node-app/build/server )

  • line 9: this is saying for root ‘/’ use the publicPath (that we have defined on line 6, which should equal (.)/ssr-react-node-app/build)
  • line 11: when we hit localhost:8080/ render index.html (notice when defining indexPath (line 7), we wrote ‘../index.html’. That is because ‘dirname = (.)/ssr-react-node-app/build/server’ and index is at ‘(.*)/ssr-react-node-app/build/index.js’, so we need to go back into the build folder!).

THIS IS HOW WE RENDER OUR REACT APP ON THE SERVER SIDE!

Our server/index.js file

Nearly there!

BUT WAIT (I hear you shout)… IT USES ES6 AND IT HASN’T BEEN TRANSPILED!

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!

Also, previously, webpack-dev-server compiled the files in memory for us, but now we will have to create a script to build these compiled files.

In the scripts section lets write a command to transpile (‘build’) our server side code and another for our client side. Lets call it “build:server” and build:client”:

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

"start:dev:client": "webpack-dev-server"
}

We have told babel to get the server 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:server”:

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

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 run our app all in dev mode using our own server. Lets call it “start:dev”:

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

Now in your terminal run

$ npm run start:dev

WOOP WOOP!!! YOU MADE IT!!!!

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

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

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.

I plan to add css and tests (as I am a big advocate for TDD). I will use jest and enzyme predominantly.


Tag cloud