We’re underway with the development of our client side (front end) application with React.js after the last episode in this series covering the set up with Webpack. Now it’s time to create an application server with Express and structure the code to run as an isomorphic application. An isomorphic application is one where we share the application logic on both the client and the server. In this case that is the React.js components which will be rendered on the server using Express. The benefits of this reach beyond just code sharing as it helps us with Search Engine Optimisation, improving page load times and providing a richer user experience.
If you followed the first article in this series then you will already have Express installed. If not you should work through that article first or install it before continuing. Alternatively you can get up to speed by downloading the code for the last project. If doing this you will need to run npm install to get the dependencies installed.
We start by creating a new directory in the project folder named server. Add a new file inside the server directory named index.js with the following code.
1 2 3 4 5 6 7 8 9 |
import express from 'express'; const app = express(); app.get('/', (request, response) => { response.send('Hello world from Express'); }); app.listen(3000, () => console.log('Server running')); |
Here we import express and call it to create a new application named app. Next we define a route for the application which is ‘/’. What this means is that when we visit the application without specifying a specific page or section the function we pass as the second parameter will be called to handle the request and provide an appropriate response, the text that is sent back to the client/browser. We use an arrow function that receives two arguments, the request object being received and the response object we will be sending back. For now we send back the message ‘Hello world from Express’.
Before we can run this file and test our server we must install the Babel CLI (the command line tools for Babel). We need to install these as we are writing the Node.js server code in the ES2015 syntax which, at the time of writing, is not fully supported in the most recent version of Node.js.
1 |
npm install babel-cli -g |
We install the babel-cli globally like we did with Webpack but the core babel library that the CLI tools will use was installed locally to our project in the last article. Once installed we need to create a configuration file for babel in ./.babelrc (note the file name starts with a dot). We do this to tell babel which presets we require to build our code and to ignore packages in the node_modules folder.
1 2 3 4 |
{ "presets": ["react", "es2015"], "ignore": "node_modules" } |
Run the server from the root of the project directory using the babel-node command.
1 |
babel-node server |
This command will compile the ./server/index.js file using Babel and start the server. After a few seconds you should see the message ‘Server running’ in the terminal window. When you see this message visit http://localhost:3000 in your browser where you should be greeted with the message “Hello world f rom Express”.
We have a server running with very minimal effort thanks to Express! Kill the server now by hitting Ctrl-C in the terminal window where it is running.
Building React components for Express
Now that we have a server we need to look at how we will share our React.js components between the server and the client. There is going to be code outside of the components themselves that we won’t be sharing such as the code to fetch data as this will be handled differently in the two environments. What we do want to share is our React.js components and the data flow to update and render those components with Redux (we’ll be introducing Redux and RethinkDB in the next article).
Webpack enables us to build code for both the client and server environments. On the client we will build the entire application from the index.js entry point but for the server we will build the code in chunks from the top level (smart) components. We currently only have a single component ./client/components/app so lets set up Webpack to build this chunk for the server. Make the following changes to the existing webpack.config.js file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
const path = require('path'); const CLIENT_DIR = path.resolve(__dirname, 'client'); const SERVER_DIR = path.resolve(__dirname, 'server/generated'); const DIST_DIR = path.resolve(__dirname, 'dist'); const loaders = [{ test: /\.js$/, include: CLIENT_DIR, loader: 'babel-loader', query: { presets: ['es2015', 'react'] } }, { test: /\.less$/, loader: 'style-loader!css-loader!less-loader' } ]; module.exports = [{ name: 'client', target: 'web', context: CLIENT_DIR, entry: './index.js', output: { path: DIST_DIR, filename: 'bundle.js' }, module: { loaders: loaders }, resolve: { alias: { components: path.resolve(CLIENT_DIR, 'components') } } }, { name: 'server', target: 'node', context: CLIENT_DIR, entry: { app: 'components/app/index.js' }, output: { path: SERVER_DIR, filename: '[name].js', libraryTarget: 'commonjs2' }, externals: /^[a-z\-0-9]+$/, module: { loaders: loaders }, resolve: { alias: { components: path.resolve(CLIENT_DIR, 'components') } } }]; |
Added here is a second set of configuration that targets node. This means that the code to be built is for use with Node.js. We define the entry point in the server configuration as an object which contains the names of the chunks we want to build. For now this just contains the app component. In the output section we specify that files should be saved in the directory ./server/generated and that each file be named according to the name given in the entry section using the ‘[name].js’ placeholder. We also specify that the output library type of the files should be commonjs2 the module system used in Node.js.
Running webpack now will show the build status for both sets of configuration. The client code is now saved into the file ./dist/bundle.js so you can delete the old ./bundle.js file from the previous tutorials if it exists. The server code is saved into the ./server/generated/app.js file ready for use in our Express server. We have an issue though, if we try to import and use the App component as it is now then we will receive an error. This error is due to the import of ‘./style.less’ which makes no sense to be importing in server side code. To deal with this issue we will extract the generated CSS into a separate file so that it can be included from our index.html page. To do this we install and configure the the extract-text-webpack-plugin to remove the output of the style-loader and save it into an external CSS file.
1 |
npm install extract-text-webpack-plugin --save-dev |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
const path = require('path'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); const CLIENT_DIR = path.resolve(__dirname, 'client'); const SERVER_DIR = path.resolve(__dirname, 'server/generated'); const DIST_DIR = path.resolve(__dirname, 'dist'); const loaders = [{ test: /\.js$/, include: CLIENT_DIR, loader: 'babel-loader', query: { presets: ['es2015', 'react'] } }, { test: /\.less$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader!less-loader') } ]; module.exports = [{ name: 'client', target: 'web', context: CLIENT_DIR, entry: './index.js', output: { path: DIST_DIR, filename: 'bundle.js' }, module: { loaders: loaders }, resolve: { alias: { components: path.resolve(CLIENT_DIR, 'components') } }, plugins: [ new ExtractTextPlugin('bundle.css', {allChunks: true}) ] }, { name: 'server', target: 'node', context: CLIENT_DIR, entry: { app: 'components/app/index.js' }, output: { path: SERVER_DIR, filename: '[name].js', libraryTarget: 'commonjs2' }, externals: /^[a-z\-0-9]+$/, module: { loaders: loaders }, resolve: { alias: { components: path.resolve(CLIENT_DIR, 'components') } }, plugins: [ new ExtractTextPlugin('[name].css') ] }]; |
Run webpack again. We now have ./dist/bundle.css alongside ./dist/bundle.js and ./server/generated/app.css alongside ./server/generated/app.js with all CSS extracted from our JavaScript code for both environments.
Using React.js components on the server
Now we are able to import and use our App component in our Express application.
1 2 3 4 5 6 7 8 9 10 11 12 |
import express from 'express'; import React from 'react'; import ReactDOMServer from 'react-dom/server'; import App from './generated/app'; const app = express(); app.get('/', (request, response) => { response.send(ReactDOMServer.renderToString(<App />)); }); app.listen(3000, () => console.log('Server running')); |
Start the express server again.
1 |
babel-node server |
Open the page once again in the web browser at http://localhost:3000 and you should see the message “Hello world from a React component”. Congratulations! You just created the first part or an isomorphic application with Node.js. Now lets get the application back to the same state as it was at the end of the last article with the CSS and index.html page.
Adding templates to Express
Currently we are just sending the string rendered by the App component as the response to the request we receive from browser but we need to send our index.html file with the component string inserted within the main tag. To do this we are going to install and use a template library, in this case we will be using the Handlebars library for Express. Install and add it to the application now.
1 |
npm install express-handlebars --save |
Edit ./server/index.js to register Handlebars as the view engine for our application. Doing this tells express how we want to render output for our responses.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import express from 'express'; import handlebars from 'express-handlebars'; import React from 'react'; import ReactDOMServer from 'react-dom/server'; import App from './generated/app'; const app = express(); app.engine('handlebars', handlebars({defaultLayout: 'main'})); app.set('view engine', 'handlebars'); app.get('/', (request, response) => { response.render('app', { app: ReactDOMServer.renderToString(<App />) }); }); app.listen(3000, () => console.log('Server running')); |
We start by adding Handlebars as a template engine using the engine method on the app object before telling the app to use this engine as the view engine. In doing this we set the default layout to be main which is going to be the main parts of the index.html file created in the previous articles. Layouts are used to provide a HTML structure that is common to a number of pages. Create the directory ./server/views/layouts and move ./index.html to become ./server/views/layouts/main.handlebars. Edit main.handlebars so that it now looks like the below.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> <title>React isomorphic</title> <link rel="stylesheet" href="bundle.css"> </head> <body> {{{body}}} <script src="bundle.js" async></script> </body> </html> |
The {{{body}}} placeholder is where the contents of each view will be rendered. So far we only have a single view which is the result of rendering our App component but as the application grows we will add additional routes with additional views. Add the app view in the file ./server/views/app.handlebars and copy the below into it.
1 |
<main id="app">{{{app}}}</main> |
Save the file and run the application but this time run the application from inside of the server directory. Change the current directory before trying to run the server.
1 2 |
cd server babel-node ./index.js |
As before you should see the rendered text “Hello world from a React component” in the browser but there is a problem. If you look in the network tab of your browsers development tools you will see that both bundle.js and bundle.css return a 404 not found. This is because express is not yet configured to serve these static assets. Let’s configure express.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import path from 'path'; import express from 'express'; import handlebars from 'express-handlebars'; import React from 'react'; import ReactDOMServer from 'react-dom/server'; import App from './generated/app'; const app = express(); // View templates app.engine('handlebars', handlebars({defaultLayout: 'main'})); app.set('view engine', 'handlebars'); // Static assets app.use(express.static(path.resolve(__dirname, '../dist'))); // Routes app.get('/', (request, response) => { response.render('app', { app: ReactDOMServer.renderToString(<App />) }); }); app.listen(3000, () => console.log('Server running')); |
We tell express to use the static assets from the ./dist folder generated by Webpack and add some comments as our code is now starting to grow. At a later stage we will split the code out so that it is more easily understood and maintainable.
Stop the server if it is already running, ensure both bundle.js and bundle.css are built by running Webpack then restart the server. Opening the page in the browser now should show the rendered component with the styling applied.
If you’ve enjoyed the series so far why not sign up for free and receive updates about new article as we release them.
Well, this certainly helped me out, I have a great starting point for my project, can’t wait to see what you have coming next.
Great, thanks. I’m glad you are finding them useful.
For anyone else currently receiving an error when attempting to bundle the initial App component for the server – the issue seems to be due to Babel. This re-write works:
from: https://github.com/babel/babel/issues/2694
Great article series by the way 🙂
Thanks Ali, yes you are right about this issue and I will update the code structure in the next post due out this week. A more ES6 solution to this though would be to export the class beneath the declaration.
Upon trying to run babel-node, I get a nasty error:
/Users/jimwharton/Sites/iso/server/index.js:29
__source: {
^^^^^^^^
SyntaxError: Duplicate data property in object literal not allowed in strict mode
at exports.runInThisContext (vm.js:73:16)
at Module._compile (module.js:443:25)
at loader (/usr/local/lib/node_modules/babel-cli/node_modules/babel-register/lib/node.js:128:5)
at Object.require.extensions.(anonymous function) [as .js] (/usr/local/lib/node_modules/babel-cli/node_modules/babel-register/lib/node.js:138:7)
at Module.load (module.js:355:32)
at Function.Module._load (module.js:310:12)
at Function.Module.runMain (module.js:501:10)
at /usr/local/lib/node_modules/babel-cli/lib/_babel-node.js:161:27
at Object. (/usr/local/lib/node_modules/babel-cli/lib/_babel-node.js:162:7)
at Module._compile (module.js:460:26)
I’m wondering if there’s something messed up in babel-cli at the moment.
Not sure what __source is in your server code but you could eliminate possible problems from external packages by ignoring node_modules in .babelrc. I should have added that anyway so will update the example now.
Thanks for your reply. It turned out to be some nasty flux (no relation to the pattern) going on in babel-core. It appears to be handled after 6.1.4.
Thanks for a fantastic tutorial.
“Once installed we need to create a configuration file for babel in ./.babelrc (not the file name starts with a dot).”
I think you missed a letter here in the parentheses, “e”.
Great article once again!
“Create the directory ./server/views/layouts and move ./index.html to become ./server/views/layouts/main.hanblebars.”
Typo here with the file extension. 🙂
Thanks, I’ve updated both typos.
Great stuff!
I started out with Express recently but felt I needed some “pazazz” and thought about React (since everyone’s talking about it). Found some tuts. but none that was this thorough, concise and clear.
You also used Handlebars which is great (although I used that with my Express app, the files I named index.hbs and not index.handlebars, go figure). I was glad to see how I could just implement React, with your help, into my Express app this way (the tutorial).
10/10 to you and this article serie.
Ps. Switched Less for Stylus which I prefer, all matter of taste.
Since the webpack and loader updates, this tutorial seems to be broken. Code for code, the following error is thrown and I have not been able to fix it.
localhost/:8 GET http://localhost:3000/bundle.css%22 net::ERR_ABORTED
bundle.js:344 Uncaught Error: Target container is not a DOM element.
at invariant (bundle.js:344)
at renderSubtreeIntoContainer (bundle.js:20144)
at Object.render (bundle.js:20214)
at Object. (bundle.js:978)
at __webpack_require__ (bundle.js:20)
at module.exports (bundle.js:63)
at bundle.js:66
invariant @ bundle.js:344
renderSubtreeIntoContainer @ bundle.js:20144
render @ bundle.js:20214
(anonymous) @ bundle.js:978
__webpack_require__ @ bundle.js:20
module.exports @ bundle.js:63
(anonymous) @ bundle.js:66
Thanks for this great article! I’ve been pulling my hair out on how to implement SSR for an existing React SPA. I tried a number of webpack components like universal-webpack and none seemed to work properly. This is a fairly simple solution that leverages node configuration of webpack. I ran into some headaches with node executing some code for old school jquery plugins, but was able to just ignore that code using the webpack ignore loader (https://www.npmjs.com/package/ignore-loader). I’d love to see in the future an article on how to implement SSR with a more complete solution using all of the goods: Redux, React Router v4 and legacy jquery code.