Hi Bro,
Today we are going to learn how to build a simple “Universal JavaScript” app (a.k.a. “Isomorphic”) using React Router, Express and React,
Enjoy your passion and… it’s going to be fun now!
About the author
Hi, I am Luciano and I am the co-author of Node.js Design Patterns Second Edition (Packt), a book that will take you on a journey across various ideas and components, and the challenges you would commonly encounter while designing and developing software using the Node.js platform. In this book you will discover the “Node.js way” of dealing with design and coding decisions. It also features an entire chapter dedicated to Universal JavaScript. If this is the first time you read this term, keep reading, you are going to love this article!About Universal JavaScript
One of the advantages of having Node.js as runtime for the backend of a web application is that we have to deal only with JavaScript as a single language across the web stack. With this capability it is totally legit to be willing to share some code between the frontend and the backend to reduce the code duplication between the browser and the server to the bare minimun. The art of creating JavaScript code that is “environment agnostic” is today being recognized as “Universal JavaScript”, term that — after a very long debate — seems to have won a war against the original name “Isomorphic JavaScript”.The main concerns that we generally have to face when building an Universal JavaScript application are:
- Module sharing: how to use Node.js modules also in the browser.
- Universal rendering: how to render the views of the application from the server (during the initialization of the app) and then keep rendering the other views directly in the browser (avoiding a full page refresh) while the user keep navigating across the diffent sections.
- Universal routing: how to recognize the view associated to the current route from both the server and the browser.
- Universal data retrival: how to access data (typically through APIs) from both the server and the browser.
In this article we are going to use React (with its companion library React Router) and Express to build a simple application focused on showcasing universal rendering and routing. We will also use Babel to take advantage of the lovely EcmaScript 2015 syntax and Webpack to build our code for the browser.
What we are going to build
I am a Judo fan and so the app we are going to build today is “Judo Heroes”, a web app that showcases some the most famous Judo athletes and their collection of medals from the Olympic Games and other prestigious international tournaments.This app has essentially two views:
An index page where you can select the athletes:
And an athlete page that showcases their medals and some other details:
To understand better how it works you can have a look at the demo app and navigate across the views.
What’s the matter with it anyway, you are probably asking yourself! Yes, it looks like a very simple app, with some data and a couple of views…
Well there’s something very peculiar that happens behind the scenes that will be hardly noticed by a regular user but it makes developement super interesting: this app is using universal rendering and routing!
We can prove this using the developers tools of the browser. When we initially load a page in the browser (any page, not necessarily the home page, try for example this one) the server provides the full HTML code of the view and the browser only needs to download linked resources (images, stylesheets and scripts):
Then, from there, when we switch to another view, everything happens only on the browser: no HTML code is loaded from the server and only the new resources (3 new images in the following example) are loaded by the browser:
We can do another quick test (if you are still not convinced) from the command line using curl:
curl -sS "https://judo-heroes.herokuapp.com/athlete/teddy-riner"
You will see the full HTML page (including the code rendered by React) being generated directly from the server:I bet you are now convinced enough and eager to get your hands dirty, so let’s start coding!
Folder structure
At the end of this tutorial our project structure will look like in the following tree:├── package.json
├── webpack.config.js
├── src
│ ├── app-client.js
│ ├── routes.js
│ ├── server.js
│ ├── components
│ │ ├── AppRoutes.js
│ │ ├── AthletePage.js
│ │ ├── AthletePreview.js
│ │ ├── AthletesMenu.js
│ │ ├── Flag.js
│ │ ├── IndexPage.js
│ │ ├── Layout.js
│ │ ├── Medal.js
│ │ └── NotFoundPage.js
│ ├── data
│ │ └── athletes.js
│ ├── static
│ │ ├── index.html
│ │ ├── css
│ │ ├── favicon.ico
│ │ ├── img
│ │ └── js
│ └── views
` └── index.ejs
In the main level we have our package.json
(to describe the project and define the dependencies) and webpack.config.js
(Webpack configuration file).All the rest of the code will be stored inside the folder
src
, which contains the main files needed for routing (routes.js
) and rendering (app-client.js
and server.js
). It also contains 4 subfolders:components
: contains all the React componentsdata
: contains our data “module”static
: contains all the static files needed for our application (css, js, images, etc.) and anindex.html
that we will use initially to test our app.views
: contains the template that we will use from the server to render the HTML content from the server.
Project initialization
The only requisite here is to have Node.js (version 6 is preferred) and NPM installed in your machine.Let’s create a new folder called
judo-heroes
somewhere in the disk and point the terminal there, then launch:npm init
This will bootstrap our Node.js project allowing us to add all the needed dependencies.We will need to have babel, ejs, express, react and react-router installed. To do so you can run the following command:
npm install --save babel-cli@6.11.x babel-core@6.13.x \
babel-preset-es2015@6.13.x babel-preset-react@6.11.x ejs@2.5.x \
express@4.14.x react@15.3.x react-dom@15.3.x react-router@2.6.x
We will also need to install Webpack(with its Babel loader extension) and http-server as a development dependencies:npm install --save-dev webpack@1.13.x babel-loader@6.2.x http-server@0.9.x
The HTML boilerplate
From now on, I am assuming you have a basic knowledge of React and JSX and its component based approach. If not you can read an excellent article on React components or have a look at all the other React related articles on Codequs.com.Initially we will focus only on creating a functional “Single Page Application” (with only client side rendering). Later we will see how to improve it by adding universal rendering and routing.
So the first thing we need is an HTML boilerplate to “host” our app that we will store in
src/static/index.html
:<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Judo Heroes - A Universal JavaScript demo application with React</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div id="main"></div>
<script src="/js/bundle.js"></script>
</body>
</html>
Nothing special here. Only two main things to underline:- We are using a simple “hand-made” stylesheet that you might want to download and save it under
src/static/css/
. - We also reference a
/js/bundle.js
file that contains all our JavaScript frontend code. We will see later in the article how to generate it using Webpack and Babel, so you don’t need to worry about it now.
The data module
In a real world application we would probably use an API to obtain the data necessary for our application.In this case we have a very small dataset with only 5 athletes and some related information, so we can keep things simple and embed the data into a JavaScript module. This way we can easily import the data into any other component or module synchronously, avoiding the added complexity and the pitfalls of managing asynchronous APIs in an Universal JavaScript project, which is not the goal of the article.
Let’s see how the module looks like:
// src/data/athletes.js
const athletes = [
{
'id': 'driulis-gonzalez',
'name': 'Driulis González',
'country': 'cu',
'birth': '1973',
'image': 'driulis-gonzalez.jpg',
'cover': 'driulis-gonzalez-cover.jpg',
'link': 'https://en.wikipedia.org/wiki/Driulis_González',
'medals': [
{ 'year': '1992', 'type': 'B', 'city': 'Barcelona', 'event': 'Olympic Games', 'category': '-57kg' },
{ 'year': '1993', 'type': 'B', 'city': 'Hamilton', 'event': 'World Championships', 'category': '-57kg' },
{ 'year': '1995', 'type': 'G', 'city': 'Chiba', 'event': 'World Championships', 'category': '-57kg' },
{ 'year': '1995', 'type': 'G', 'city': 'Mar del Plata', 'event': 'Pan American Games', 'category': '-57kg' },
{ 'year': '1996', 'type': 'G', 'city': 'Atlanta', 'event': 'Olympic Games', 'category': '-57kg' },
{ 'year': '1997', 'type': 'S', 'city': 'Osaka', 'event': 'World Championships', 'category': '-57kg' },
{ 'year': '1999', 'type': 'G', 'city': 'Birmingham', 'event': 'World Championships', 'category': '-57kg' },
{ 'year': '2000', 'type': 'S', 'city': 'Sydney', 'event': 'Olympic Games', 'category': '-57kg' },
{ 'year': '2003', 'type': 'G', 'city': 'S Domingo', 'event': 'Pan American Games', 'category': '-63kg' },
{ 'year': '2003', 'type': 'S', 'city': 'Osaka', 'event': 'World Championships', 'category': '-63kg' },
{ 'year': '2004', 'type': 'B', 'city': 'Athens', 'event': 'Olympic Games', 'category': '-63kg' },
{ 'year': '2005', 'type': 'B', 'city': 'Cairo', 'event': 'World Championships', 'category': '-63kg' },
{ 'year': '2006', 'type': 'G', 'city': 'Cartagena', 'event': 'Central American and Caribbean Games', 'category': '-63kg' },
{ 'year': '2006', 'type': 'G', 'city': 'Cartagena', 'event': 'Central American and Caribbean Games', 'category': 'Tema' },
{ 'year': '2007', 'type': 'G', 'city': 'Rio de Janeiro', 'event': 'Pan American Games', 'category': '-63kg' },
{ 'year': '2007', 'type': 'G', 'city': 'Rio de Janeiro', 'event': 'World Championships', 'category': '-63kg' },
],
},
{
// ...
}
];
export default athletes;
For brevity the file here has been truncated, and we are displaying
just the data of one of the five athletes. If you want to see the full
code check it out on the official repository. You can download the file into src/data/athletes.js
.As you can see, the file contains an array of objects where every object represents an athlete containing some generic information like
id
, name
and country
and another array of objects representing the medals
won by that athlete.You might also want to grab all the image files from the repository and copy them under:
src/static/img/
.React components
We are going to organize the views of our application into several components:- A set of small UI components used to build the views:
AthletePreview
,Flag
,Medal
andAthletesMenu
. - A
Layout
component that is used as master component to define the generic appearence of the application (header, content and footer blocks). - Two main components that represent the main sections:
IndexPage
andAthletePage
. - An extra “page” component that we will use as 404 page:
NotFoundPage
- The
AppRoutes
component that uses React Router to manage the routing between views.
Flag component
The first component that we are going to build allows us to display a nice flag and, optionally, the name of the country that it represents:// src/components/Flag.js
import React from 'react';
const data = {
'cu': {
'name': 'Cuba',
'icon': 'flag-cu.png',
},
'fr': {
'name': 'France',
'icon': 'flag-fr.png',
},
'jp': {
'name': 'Japan',
'icon': 'flag-jp.png',
},
'nl': {
'name': 'Netherlands',
'icon': 'flag-nl.png',
},
'uz': {
'name': 'Uzbekistan',
'icon': 'flag-uz.png',
}
};
export default class Flag extends React.Component {
render() {
const name = data[this.props.code].name;
const icon = data[this.props.code].icon;
return (
<span className="flag">
<img className="icon" title={name} src={`/img/${icon}`}/>
{this.props.showName && <span className="name"> {name}</span>}
</span>
);
}
}
As you might have noticed this component uses a small array of
countries as data source. Again this makes sense only because we need a
very small data set which, for the sake of this demo app, is not going
to change. In a real application with a larger and more complex data set
you might want to use an API or a different mechanism to connect the
data to the component.In this component it’s also important to notice that we are using two different props,
code
and showName
.
The first one is mandatory and must be passed to the component to
select which flag will be shown among the ones supported. The showName
props is instead optional and if set to a truthy value the component will also display the name of the country just after the flag.If you want to build a more refined reusable component for a real world app you might also want to add to it props validation and defaults, but we are going to skip this step here as this is not the goal for the app we want to build.
Medal component
TheMedal
component is similar to the Flag
component. It receives some props that represent the data related to a medal: the type
(G
for gold, S
for silver and B
for bronze), the year
when it was won, the name of the event
and the city
where the tournament was hosted and the category
where the athlete who won the medal competed.// src/components/Medal.js
import React from 'react';
const typeMap = {
'G': 'Gold',
'S': 'Silver',
'B': 'Bronze'
};
export default class Medal extends React.Component {
render() {
return (
<li className="medal">
<span className={`symbol symbol-${this.props.type}`} title={typeMap[this.props.type]}>{this.props.type}</span>
<span className="year">{this.props.year}</span>
<span className="city"> {this.props.city}</span>
<span className="event"> ({this.props.event})</span>
<span className="category"> {this.props.category}</span>
</li>
);
}
}
As for the previous component here we also use a small object to map the codes of the medal types to descriptive names.Athletes Menu component
In this section we are going to build the menu that is displayed on top of every athlete page to allow the user to easily switch to another athlete without going back to the index:// src/components/AthletesMenu.js
import React from 'react';
import { Link } from 'react-router';
import athletes from '../data/athletes';
export default class AthletesMenu extends React.Component {
render() {
return (
<nav className="atheletes-menu">
{athletes.map(menuAthlete => {
return <Link key={menuAthlete.id} to={`/athlete/${menuAthlete.id}`} activeClassName="active">
{menuAthlete.name}
</Link>;
})}
</nav>
);
}
}
The component is very simple, but there are some key points to underline:- We are importing our data module directly into the component to have access to the list of athletes available in the app.
- We use the
map
method to iterate over all the athletes and generate for every one of them aLink
. Link
is a special component provided by React Router to create links between views.- Finally, we use the prop
activeClassName
to use the classactive
when the current route matches the path of the link.
Athlete Preview component
TheAthletePreview
component is used in the index to display the pictures and the names of the athletes. Let’s see its code:// src/components/AthletePreview.js
import React from 'react';
import { Link } from 'react-router';
export default class AthletePreview extends React.Component {
render() {
return (
<Link to={`/athlete/${this.props.id}`}>
<div className="athlete-preview">
<img src={`img/${this.props.image}`}/>
<h2 className="name">{this.props.name}</h2>
<span className="medals-count"><img src="/img/medal.png"/> {this.props.medals.length}</span>
</div>
</Link>
);
}
}
The code is quite simple. We expect to receive a number of props that
describe the attributes of the athlete we want to display like id
, image
, name
and medals
. Note that again we are using the Link
component to create a link to the athlete page.Layout component
Now that we built all our basic components let’s move to creating those that give the visual structure to the application. The first one is theLayout
component, which has the only purpose of
providing a display template to the whole application defining an
header, a space for the main content and a footer:// src/components/Layout.js
import React from 'react';
import { Link } from 'react-router';
export default class Layout extends React.Component {
render() {
return (
<div className="app-container">
<header>
<Link to="/">
<img className="logo" src="/img/logo-judo-heroes.png"/>
</Link>
</header>
<div className="app-content">{this.props.children}</div>
<footer>
<p>
This is a demo app to showcase universal rendering and routing with <strong>React</strong> and <strong>Express</strong>.
</p>
</footer>
</div>
);
}
}
The component is pretty simple and we should understand how it works
just by looking at the code. There’s though a very interesting prop that
we are using here, the children
prop. This is a special property that React provides to every component and allows to nest components one inside another.We are going to see in the routing section how the React Router will make sure to nest the components into the
Layout
component.Index Page component
This component constitutes the full index page and it contains some of the components we previously defined:// src/components/IndexPage.js
import React from 'react';
import AthletePreview from './AthletePreview';
import athletes from '../data/athletes';
export default class IndexPage extends React.Component {
render() {
return (
<div className="home">
<div className="athletes-selector">
{athletes.map(athleteData => <AthletePreview key={athleteData.id} {...athleteData} ></AthletePreview>)}
</div>
</div>
);
}
}
Note that in this component we are using the AthletePreview
component we created previously. Basically we are iterating over all
the available athletes from our data module and creating an AthletePreview
component for each of them. The AthletePreview
component is data agnostic, so we need to pass all the information about the current athlete as props using the JSX spread operator ({...object}
).Athlete Page component
In a similar fashion we can define theAthletePage
component:// src/components/AthletePage.js
import React from 'react';
import { Link } from 'react-router';
import NotFoundPage from './NotFoundPage';
import AthletesMenu from './AthletesMenu';
import Medal from './Medal';
import Flag from './Flag';
import athletes from '../data/athletes';
export default class AthletePage extends React.Component {
render() {
const id = this.props.params.id;
const athlete = athletes.filter((athlete) => athlete.id === id)[0];
if (!athlete) {
return <NotFoundPage></NotFoundPage>;
}
const headerStyle = { backgroundImage: `url(/img/${athlete.cover})` };
return (
<div className="athlete-full">
<AthletesMenu></AthletesMenu>
<div className="athlete">
<header style={headerStyle}></header>
<div className="picture-container">
<img src={`/img/${athlete.image}`}/>
<h2 className="name">{athlete.name}</h2>
</div>
<section className="description">
Olympic medalist from <strong><Flag code={athlete.country} showName="true"></Flag></strong>,
born in {athlete.birth} (Find out more on <a href={athlete.link} target="_blank">Wikipedia</a>).
</section>
<section className="medals">
<p>Winner of <strong>{athlete.medals.length}</strong> medals:</p>
<ul>{
athlete.medals.map((medal, i) => <Medal key={i} {...medal}></Medal>)
}</ul>
</section>
</div>
<div className="navigateBack">
<Link to="/">« Back to the index</Link>
</div>
</div>
);
}
}
By now, you must be able to understand most of the code shown here
and how the other components are used to build this view. What might be
important to underline is that this page component accepts from the
outside only the id of the athlete, so we include the data module to be
able to retrieve the related information. We do this at the beginning of
the render
method using the function filter
on the data set. We are also considering the case where the received id
does not exist in our data module, in this case we render NotFoundPage
, a component that we are going to create in the next section.One last important detail is that here we are accessing the id with
this.props.params.id
(instead of simply this.props.id
): params
is a special object created by React Router when using a component from a Route
and it allows to propagate routing parameters into components. It will
be easier to understand this concept when we will see how to setup the
routing part of the application.Not Found Page component
Now let’s see theNotFoundPage
component, which acts as a template to generate the code of our 404 pages:// src/components/NotFoundPage.js
import React from 'react';
import { Link } from 'react-router';
export default class NotFoundPage extends React.Component {
render() {
return (
<div className="not-found">
<h1>404</h1>
<h2>Page not found!</h2>
<p>
<Link to="/">Go back to the main page</Link>
</p>
</div>
);
}
}
App Routes component
The last component we need to create is theAppRoutes
component which is the master component that renders all the other views
using internally the React Router. This component will use the routes
module, so let’s have a quick look at it first:// src/routes.js
import React from 'react'
import { Route, IndexRoute } from 'react-router'
import Layout from './components/Layout';
import IndexPage from './components/IndexPage';
import AthletePage from './components/AthletePage';
import NotFoundPage from './components/NotFoundPage';
const routes = (
<Route path="/" component={Layout}>
<IndexRoute component={IndexPage}></IndexRoute>
<Route path="athlete/:id" component={AthletePage}></Route>
<Route path="*" component={NotFoundPage}></Route>
</Route>
);
export default routes;
In this file we are basically using the React Router Route
component to map a set of routes to the page components we defined before. Note how the routes are nested inside a main Route
component. Let’s explain how this works:- The root route maps the path
/
to theLayout
component. This allows us to use our custom layout in every section of our application. The components defined into the nested routes will be rendered inside theLayout
component in place of thethis.props.children
property that we discussed before. - The first child route is an
IndexRoute
which is a special route used to define the component that will be rendered when we are viewing the index page of the parent route (/
in this case). We use ourIndexPage
component as index route. - The path
athlete/:id
is mapped to theAthletePage
. Note here that we are using a named parameter:id
. So this route will match all the paths with the prefix/athlete/
, the remaining part will be associated to the paramsid
and will be available inside the component inthis.props.params.id
. - Finally the match-all route
*
maps every other path to theNotFoundPage
component. This route must be defined as the last one.
AppRoutes
component:// src/components/AppRoutes.js
import React from 'react';
import { Router, browserHistory } from 'react-router';
import routes from '../routes';
export default class AppRoutes extends React.Component {
render() {
return (
<Router history={browserHistory} routes={routes} onUpdate={() => window.scrollTo(0, 0)}/>
);
}
}
Basically we only need to import the Router
component and add it inside our render
function. The router receives our routes mapping in the router
prop. We also configure the history
prop to specify that we want to use the HTML5 browser history for the routing (as an alternative you could also use hashHistory).Finally we also added an
onUpdate
callback to reset the scrolling of the window to the top everytime a link is clicked.The application entry point
The last bit of code to complete our first version of the application is to define the JavaScript logic that initializes the whole app in the browser:// src/app-client.js
import React from 'react';
import ReactDOM from 'react-dom';
import AppRoutes from './components/AppRoutes';
window.onload = () => {
ReactDOM.render(<AppRoutes></AppRoutes>, document.getElementById('main'));
};
The only thing we do here is to import our master AppRoutes
component and render it using the ReactDOM.render
method. The React app will be living inside our #main
DOM element.Setting up Webpack and Babel
Before we are able to run our application we need to generate thebundle.js
file containing all our React components with Webpack. This file will
be executed by the browser so Webpack will make sure to convert all the
modules into code that can be executed in the most common browser
environments. Webpack will convert ES2015 and React JSX syntax to
equivalent ES5 syntax (using Babel), which can be executed
practically by every browser. Furthermore we can use Webpack to apply a
number of optimizations to the resulting code like combining all the
scripts files into one file and minifying the resulting bundle.Let’s write our webpack configuration file:
// webpack.config.js
const webpack = require('webpack');
const path = require('path');
module.exports = {
entry: path.join(__dirname, 'src', 'app-client.js'),
output: {
path: path.join(__dirname, 'src', 'static', 'js'),
filename: 'bundle.js'
},
module: {
loaders: [{
test: path.join(__dirname, 'src'),
loader: ['babel-loader'],
query: {
cacheDirectory: 'babel_cache',
presets: ['react', 'es2015']
}
}]
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
}),
new webpack.optimize.DedupePlugin(),
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.optimize.UglifyJsPlugin({
compress: { warnings: false },
mangle: true,
sourcemap: false,
beautify: false,
dead_code: true
})
]
};
In the first part of the configuration file we define what is the
entry point and the otput file. The entry point is the main JavaScript
file that initializes the application. Webpack will recursively resolve
all the included/imported resources to determine which files will go
into the final bundle.The
module.loaders
section allows to specify transformations on specific files. Here we want to use Babel with the react
and es2015
presets to convert all the included JavaScript files to ES5 code.In the final section we use
plugins
to declare and configure all the optimizations plugins we want to use:DefinePlugin
allows us to define theNODE_ENV
variable as a global variable in the bundling process as if it was defined in one of the scripts. Some modules (e.g. React) relies on it to enable or disable specific features for the current environment (production or development).DedupePlugin
removes all the duplicated files (modules imported in more than one module).OccurenceOrderPlugin
helps in reducing the file size of the resulting bundle.UglifyJsPlugin
minifies and obfuscates the resulting bundle using UglifyJs.
NODE_ENV=production node_modules/.bin/webpack -p
The NODE_ENV
environment variable and the -p
option are used to generate the bundle in production mode, which will apply a number of additional optimizations, for example removing all the debug code from the React library.If everything went fine you will now have your bundle file in
src/static/js/bundle.js
.Playing with the single page app
We are finally ready to play with the first version of our app!We don’t have a Node.js web server yet, so for now we can just use the module
http-server
(previously installed as development dependency) to run a simple static file web server:node_modules/.bin/http-server src/static
And your app will be magically available on http://localhost:8080.Ok, now take some time to play with it, click on all the links and explore all the sections.
Does everything seem to work allright? Well, almost! There’s just a little caveat… If you refresh the page in a section different from the index you will get a 404 error from the server.
There are a number of ways to address this problem. In our case it will be solved as soon as we implement our universal routing and rendering solution, so let’s move on to the next section.
Routing and rendering on the server with Express
Ok, we are now ready to evolve our application to the next level and build the missing server side part.In order to have server side routing and rendering we will use Express with a relatively small server script that we will see in a moment.
The rendering part will use an ejs template as replacement for our
index.html
file that we will save in src/views/index.ejs
:<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Judo Heroes - A Universal JavaScript demo application with React</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div id="main"><%- markup -%></div>
<script src="/js/bundle.js"></script>
</body>
</html>
The only difference with the original HTML file is that we are using the template variable <%- markup -%>
inside the #main
div in order to include the React markup into the server-generated HTML code.Now we are ready to write our server application:
// src/server.js
import path from 'path';
import { Server } from 'http';
import Express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { match, RouterContext } from 'react-router';
import routes from './routes';
import NotFoundPage from './components/NotFoundPage';
// initialize the server and configure support for ejs templates
const app = new Express();
const server = new Server(app);
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// define the folder that will be used for static assets
app.use(Express.static(path.join(__dirname, 'static')));
// universal routing and rendering
app.get('*', (req, res) => {
match(
{ routes, location: req.url },
(err, redirectLocation, renderProps) => {
// in case of error display the error message
if (err) {
return res.status(500).send(err.message);
}
// in case of redirect propagate the redirect to the browser
if (redirectLocation) {
return res.redirect(302, redirectLocation.pathname + redirectLocation.search);
}
// generate the React markup for the current route
let markup;
if (renderProps) {
// if the current route matched we have renderProps
markup = renderToString(<RouterContext {...renderProps}></RouterContext>);
} else {
// otherwise we can render a 404 page
markup = renderToString(<NotFoundPage></NotFoundPage>);
res.status(404);
}
// render the index template with the embedded React markup
return res.render('index', { markup });
}
);
});
// start the server
const port = process.env.PORT || 3000;
const env = process.env.NODE_ENV || 'production';
server.listen(port, err => {
if (err) {
return console.error(err);
}
console.info(`Server running on http://localhost:${port} [${env}]`);
});
The code is commented, so it shouldn’t be hard to get a general understanding of what is going on here.The important part of the code here is the Express route defined with
app.get('*', (req, res) => {...})
. This is an Express catch-all
route that will intercept all the GET requests to every URL in the
server. Inside this route, we take care of delegating the routing logic
to the React Router match
function.ReactRouter.match
accepts two parameters: the first one is a configuration object and the second is a callback function. The configuration object must have two keys:routes
: used to pass the React Router routes configuration. Here, we are passing the exact same configuration that we used for the client-side rendering.location
: This is used to specify the currently requested URL.
error
, redirectLocation
, and renderProps
, that we can use to determine what exactly the result of the match operation was.We can have four different cases that we need to handle:
- The first case is when we have an error during the routing resolution. To handle this case, we simply return a 500 internal server error response to the browser.
- The second case is when we match a route that is a redirect route. In this case, we need to create a server redirect message (302 redirect) to tell the browser to go to the new destination (this is not really happening in our application because we are not using redirect routes in our React Router configuration, but it’s good to have it ready in case we decide to keep evolving our application).
- The third case is when we match a route and we have to render the associated component. In this case, the argument
renderProps
is an object that contains the data we need to use to render the component. The component we are rendering isRouterContext
(contained in the React Router module), which is responsible for rendering the full component tree using the values inrenderProps
. - The last case is when the route is not matched, and here we can simply return a 404 not found error to the browser.
ReactDOM.renderToString
function to be able to render the HTML code that represents the component associated to the currently matched route.Finally, we inject the resulting HTML into the
index.ejs
template we defined before to obtain the full HTML page that we send to the browser.Now we are ready to run our
server.js
script, but because it’s using the JSX syntax we cannot simply run it with the node
interpreter. We need to use babel-node
and the full command (from the root folder of our project) looks like this:NODE_ENV=production node_modules/.bin/babel-node --presets 'react,es2015' src/server.js
Running the complete app
At this stage your app is available at http://localhost:3000 and, for the sake of this tutorial, it can be considered complete.Again feel free to check it out and try all the sections and links. You will notice that this time we can refresh every page and the server will be able to identify the current route and render the right page.
Small advice: don’t forget to check out the 404 page by entering a random non-existing URL!
Wrapping up
CodeDemo
Great stuff! Thanks..
ReplyDeleteThanks bro!
Delete