This is post is originally published on crypt.codemancers.com.
These days server side rendering has become an important feature for heavy client side applications and now most of the client side frameworks support it. Yesterday I tried a bit of react server rendering along with express.js and react-router.
Setup Express js server and dependencies.
We can start with scaffolding a new express.js app using expressjs-generator and installing all the dependencies. We use webpack
for client side
bundling.
We can install all the dependencies by running
npm i --save react react-dom react-router babel-register
and development dependencies by running
npm i --save-dev babel-cli babel-core babel-preset-es2015 babel-preset-react babel-preset-stage-0 webpack babel-loader
Setup webpack & babel for client.
We will keep all our client JavaScript and React components in a new folder named
client
and put all the compiled js in public/javascripts
. Also we shall add a
.babelrc
to load babel presets and configs.
{
"presets": ["es2015", "react", "stage-0"]
}
Now in webpack.config.js
, we will configure the entry point, output directory and babel
loader.
// webpack.config.js
var path = require('path');
module.exports = {
entry: './client/app.jsx',
output: {
filename: 'app.js',
path: path.join('public/javascripts/')
},
module: {
loaders: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
query: {
presets: ['react', 'es2015']
}
}
]
}
};
Now we can run the following to build client JavaScript files in development mode with debug flag turned on, and watch for changes.
webpack --debug --watch
For easier use, we can add the command to npm scripts with name webpack:server
. Now we just need to run npm run webpack:server
.
Setup react router
Now the basic scaffolding and development setup is finished and time to start
building our app. We can start with configuring the router. We are planning mainly for
two routes /home
to show the rendering of static component and /list
to show
the server side rendering with some data.
First we have to define the entry point which will mount our react-router
component to DOM.
// client/app.jsx
import React from 'react';
import {render} from 'react-dom';
import AppRouter from './router.jsx';
render(<AppRouter/>, document.querySelector('#app'));
Next, define routes in client/router.jsx
// client/router.jsx
import React from 'react';
import {Router, browserHistory, Route} from 'react-router';
import AppRoot from './app-root.jsx';
import Home from './home.jsx';
import List from './list.jsx';
const AppRouter = () => {
return (
<Router history={browserHistory}>
<Route path="/" component={AppRoot}>
<Route path="/home" component={Home}/>
<Route path="/list" component={List}/>
</Route>
</Router>
);
};
export default AppRouter;
AppRoot
is nothing but a simple layout for our app.
// client/app-root.jsx
import React, {Component} from 'react';
import {Link} from 'react-router';
class AppRoot extends Component {
render() {
return (
<div>
<h2>React Universal App</h2>
<Link to="/home"> Home </Link>
<Link to="/list"> List </Link>
{this.props.children}
</div>
);
}
}
export default AppRoot;
Setup express js for server side rendering
Since we are using React + ES6 for components, we have to use the babel-register
on server
side so that we can write express js routes also in ES6 and import the react routes we
already wrote. Please note that, we have to require/import the babel-register
at the beginning of
express js entry point app.js
.
// app.js
require('babel-register');
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
//rest of the express js boilerplate
Then we rename the routes/index.js
to routes/index.jsx
, after this we can use the
react routes and react components on server side. For server side rendering we use
renderToString
method from react-dom/sever
package and methods like match
, createRoutes
and RouterContext
from react-router
.
match
function in react-router
module will match a set of routes to a location and calls a callback, without rendering.
We use createRoutes
method from react-router
to create a set of routes from our client/router.jsx
(appRouter
) component and provide it to match
.
// routes/index.jsx
// express and react imports
import appRouter from '../client/router.jsx';
const routes = createRoutes(appRouter());
Once we have a match RouterContext will render the component tree for the given router state and return the component markup as a string with the help of renderToString
method.
// Express.js route
router.get('*', (req, res) => {
match({routes, location: req.url}, (error, redirectLocation, renderProps) => {
// check for error and redirection
const content = renderToString(<RouterContext {...renderProps}/>);
// pass content to jade view (we'll see it in a while)
})
})
Now we have the react components rendered as string and we need to pass this to our pug.js (Previously known as jade) view.
The jade view will accept the string in content
variable and substitute inside the react app mount point.
//- views/index.jade
extends layout
block content
script(type='text/javascript').
window.__INITIAL_STATE__ = !{JSON.stringify(data)}
div.container#app!= content
Rendering a static component on server
/home
points to a static component called Home
which we are going to render from server.
import React from 'react';
const Home = () => {
return (
<div>
<h1>Home</h1>
</div>
);
};
export default Home;
and now when we join the dots the routes/index.jsx
will look like this
import express from 'express';
import React from 'react';
import {renderToString} from 'react-dom/server';
import {RouterContext, match, createRoutes} from 'react-router';
import appRouter from '../client/router.jsx';
const routes = createRoutes(appRouter());
const router = express.Router();
router.get('/home', (req, res) => {
match({routes, location: req.url}, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message);
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search);
} else if (renderProps) {
const content = renderToString(<RouterContext {...renderProps}/>);
res.render('index', {title: 'Express', data: false, content});
} else {
res.status(404).send('Not Found');
}
});
});
Rendering component on server with data
In this section, we are trying to render a list of users, the data source is not DB but an API for demo purpose. In order to render data, we need to fetch data from the server, pass it to a component via context. For this we need to write a Higher Order Component to set the data to context.
import express from 'express';
import request from 'request';
import React, {Component} from 'react';
import {renderToString} from 'react-dom/server';
import {RouterContext, match, createRoutes} from 'react-router';
import appRouter from '../client/router.jsx';
const routes = createRoutes(appRouter());
class DataProvider extends Component {
getChildContext() {
return {data: this.props.data};
}
render() {
return <RouterContext {...this.props}/>;
}
}
DataProvider.propTypes = {
data: React.PropTypes.object
};
DataProvider.childContextTypes = {
data: React.PropTypes.object
};
The above DataProvider
will set data to context if we pass it via props named data
.
The List
component will look like,
import React, {Component} from 'react';
class List extends Component {
constructor(props, context) {
super(props, context);
this.state = this.context.data || window.__INITIAL_STATE__ || {items: []};
}
componentDidMount() {
this.fetchList();
}
fetchList() {
fetch('http://jsonplaceholder.typicode.com/users')
.then(res => {
return res.json();
})
.then(data => {
this.setState({
items: data
});
})
.catch(err => {
console.log(err);
});
}
render() {
return (
<ul>
{this.state.items.map(item => {
return <li key={item.id}>{item.name}</li>;
})}
</ul>
);
}
}
List.contextTypes = {
data: React.PropTypes.object
};
export default List;
The above list component will look for data in context first, then in global state and later in component level state. While we render it from server, the data will be available in context and component use the data in context to render the initial HTML. Later after loading it in browser the component can fetch again and update the data.
Now we can setup the route to fetch the data and render the component.
router.get('/list', (req, res) => {
match({routes, location: req.url}, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message);
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search);
} else if (renderProps) {
request('http://jsonplaceholder.typicode.com/users', (error, response, body) => {
const data = {items: JSON.parse(body)};
const content = renderToString(<DataProvider {...renderProps} data={data}/>);
res.render('index', {title: 'Express', data, content});
});
} else {
res.status(404).send('Not Found');
}
});
});
Thats it. We successfully rendered our react components from server side with and without data, so that user don’t have to wait for another ajax request after loading the page to see the data.
Thanks to codemancers team especially @emil, @kashyap and @yuva for helping to fix spelling and grammer mistakes.