4. Server rendering
Here we are: our app is working, routes are routing, actions are acting, reducers are reducing, react is reacting. But suddenly, a wild manager appears! He wants to know why Google doesn't show any contents from our website. We know the answer to this question - when Google visits our website, it sees only contents of /server/layout.html. All actual content is rendered dynamically after browser loads it. If we would have started this journey with Angular or Ember - we would be in trouble by this moment. (No disrespect - just cold truth). But with React we get server rendering for free!
To render pages on the server properly we need to:
- prepare correct state;
- pass it to our react component;
- render result into string
- and send it to the browser.
Rendering components is not hard at all, but preparing the state is a bit more interesting.
The state that we need to render any page consists of two parts - app module's state and current module's state. Current module's state is different for every page while app state is the same. Let's make a plan of how we will get correct HTML for the browser:
- Prepare
appstate. - Run router logic and determine current module.
- Prepare state for the module.
- Combine
appstate and module's state, pass it to the components, get resulting HTML. - Send HTML to the browser.
Preparing app state is very easy:
let state = { app: { ... data.app } };
But let's think for a minute. In real applications data isn't available like this - just, you know, "is there". To get data in real life we will have to query some database or another API which means one thing: more asynchronous code. We want to prepare ourselves for real applications - we have to emulate asynchronous behavior.
First, let's take a look at /server/serve.js from section 3.3:
import express from 'express';
import fs from 'fs';
import data from './data.json';
const port = 3000;
const layout = fs.readFileSync(__dirname + '/layout.html', 'utf8');
let app = express();
app.all('/api/:endpoint', (req, res) => {
setTimeout(() => {
let result;
try {
if (typeof data[req.params.endpoint] === 'undefined') throw new Error('Endpoint does not exist');
if (req.params.endpoint === 'page') {
if (!req.query.splat) throw new Error('Splat is empty');
if (!data.page[req.query.splat]) throw new Error('Splat not found');
result = data.page[req.query.splat];
} else {
result = data[req.params.endpoint];
}
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify(result));
} catch (e) {
res.status(404).send(String(e));
}
}, Math.random() * (2000 - 500) + 500);
});
app.use('/js', express.static(__dirname + '/../public/js'));
app.get('*', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.send(layout);
});
app.listen(port, () => {
console.log(`App listening at port ${port}!`);
});
We're using ExpressJS as our server. We load data from data.json and read layout file. Then we process requests to api endpoints. There we synchronously get data from data.json and send that data to the browser. We configure /js URL to point to our /public/js directory. Then we make all other requests get the contents of /server/layout.html. Finally - start listening to requests at port 3000.
To emulate asynchronous data fetching we will need to refactor the way we handle API calls. First, let's move all data manipulations into separate function that will return a promise:
/server/api.js
import data from './data.json';
const api = (endpoint, query) => {
return new Promise((resolve, reject) => {
try {
let result;
if (typeof data[endpoint] === 'undefined') throw new Error('Endpoint does not exist');
if (endpoint === 'page') {
if (!query.splat) throw new Error('Splat is empty');
if (!data.page[query.splat]) throw new Error('Splat not found');
result = data.page[query.splat];
} else {
result = data[endpoint];
}
resolve(result);
} catch (e) {
reject(e);
}
});
};
export default api;
It performs all previous checks. But instead of sending response to the browser it resolves a promise on success and rejects it on failure. Now, let's update /server/serve.js accordingly:
...
app.all('/api/:endpoint', (req, res) => {
setTimeout(() => {
api(req.params.endpoint, req.query).then(result => {
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify(result));
}).catch(e => {
res.status(404).send(String(e));
});
}, Math.random() * (2000 - 500) + 500);
});
...
Now we can start with server rendering. #1 of our plan is to prepare app state. We could do something like this:
let appData;
api('app').then(data => {
appData = data;
}).catch(e => next(e));
app.get('*', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.send(layout);
});
Declare appData variable, call api('app') and assign result to appData when promise is resolved. addData variable will be available inside the callback which sends HTML to the browser. There are problems with this approach:
- appData will be undefined until promise resolves. We don't have any guaranties someone will not access our server before that happens.
- What if api('app') fails? How do we handle this situation properly?
- appData is created when our server starts, not when somebody requests a page. It means we will have to restart the server if the data in data storage changes.
Instead, we can use ExpressJS's middleware functionality. It works like Redux's middlewares, but instead of action we get request and response objects:
app.use((req, res, next) => {
api('app').then(app => {
req.state = { app };
return next();
}).catch(e => next(e));
});
app.get('*', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.send(layout);
});
Much better! We add middleware with app.use. Inside it we get access to request and response objects and next() function, which is next middleware in chain (same as Redux). We call our api function. If it succeeds we add state field to the request object and assign our app state to it. It will be available in all further middlewares. If api function fails we call next() function with error as an argument, so we can handle it somewhere down the line.
Now req object in app.get('*') block will have state field with app state.
Next up is #2 from our plan - "run router logic and determine current module".
Exactly for this React-router has match() function. It accepts set of routes, location (requested URL in our case) and a callback. Callback is called with three arguments (just copypasting from the docs):
- error: A Javascript Error object if an error occurred, undefined otherwise.
- redirectLocation: A Location object if the route is a redirect, undefined otherwise.
- renderProps: The props you should pass to the routing context if the route matched, undefined otherwise.
Ok, first what we need to do to run match() - extract our routes from /shared/Root.jsx into a separate file. We can use them both on client and server:
/shared/routes.jsx
import React from 'react';
import { Route, Redirect } from 'react-router';
import App from './modules/app/container.jsx';
import Blog from './modules/blog/container.jsx';
import Galleries from 'modules/galleries/container.jsx';
import Page from './modules/page/container.jsx';
import Index from './modules/index/container.jsx';
export default (
<Route component={App}>
<Route path="/blog" component={Blog}/>
<Route path="/galleries" component={Galleries}/>
<Route path="/" component={Index}/>
<Route path="*" component={Page}/>
</Route>
);
We also have to update /shared/Roots.jsx:
import React from 'react';
import { Router, browserHistory } from 'react-router';
import { Provider } from 'react-redux';
import { syncHistoryWithStore } from 'react-router-redux';
import configureStore from './redux/configureStore.js';
import routes from './routes.jsx';
const store = configureStore();
const history = syncHistoryWithStore(browserHistory, store);
export default function Root() {
return (
<Provider store={store}>
<Router history={history}>
{routes}
</Router>
</Provider>
);
};
And now we can import routes into /server/serve.js file and take a look at what match() will give us:
app.get('*', (req, res) => {
match({ routes, location: req.originalUrl }, (err, redirect, renderProps) => {
console.log(JSON.stringify(renderProps, null, 4));
});
res.setHeader('Content-Type', 'text/html');
res.send(layout);
});
Here we call match and print renderProps to the console. This is what we see for /galleries route:
{
"routes": [
{
"component": "function",
"childRoutes": [
{
"path": "/blog",
"component": "function"
},
{
"path": "/galleries",
"component": "function"
},
{
"path": "/",
"component": "function"
},
{
"path": "*",
"component": "function"
}
]
},
{
"path": "/galleries",
"component": "function"
}
],
"params": {},
"location": {
"pathname": "/galleries",
...
},
"components": [
"function",
"function"
],
"history": {
"listenBefore": "function",
...
},
"router": {
"listenBefore": "function",
...
},
"matchContext": {
"history": {
"listenBefore": "function",
...
},
"transitionManager": {
"isActive": "function",
...
},
"router": {
"listenBefore": "function",
...
}
}
}
We've left the most out and still this is a big object, but at least childRoutes look familiar.
Ok, what do we do with it?
We need to find out current module's component and fetch it's data, but how do we do that?
On close examination we see components property that contains two "functions". Let's take a closer look. If we print renderProps.components[0] to the console, we will see the following:
{ [Function: Connect]
displayName: 'Connect(AppContainer)',
WrappedComponent: [Function: AppContainer],
contextTypes: { store: { [Function: bound checkType] isRequired: [Function: bound checkType] } },
propTypes: { store: { [Function: bound checkType] isRequired: [Function: bound checkType] } } }
If we print renderProps.components[1] we will see:
{ [Function: Connect]
displayName: 'Connect(GalleriesContainer)',
WrappedComponent: [Function: GalleriesContainer],
contextTypes: { store: { [Function: bound checkType] isRequired: [Function: bound checkType] } },
propTypes: { store: { [Function: bound checkType] isRequired: [Function: bound checkType] } } }
Good to see familiar faces, AppContainer and GalleriesContainer! So renderProps.components contains array of components that should be rendered for current route. We can confirm it by looking at our routing:

All appears to be correct. We're one step away from accessing fetchIfNeeded() method of GalleriesContainer component and get our state!
But what's with the displayName: "Connect(GalleriesContainer)"?
We remember that we don't export GalleriesContainer from /shared/modules/galleries/container.jpx. We export GalleriesContainer wrapped by connect() function from react-redux library. But we can still access our original component through WrappedComponent property. Ok, lets call fetchIfNeeded()method and see what happens!
console.log(renderProps.components[1].WrappedComponent.fetchIfNeeded({}));
And we get
TypeError: renderProps.components[1].WrappedComponent.fetchIfNeeded is not a function
What!? How can it be undefined?
Let's see what exactly is this WrappedComponent:
console.log(renderProps.components[1].WrappedComponent);
Prints
[Function: GalleriesContainer]
So, this is not an instance of a GalleriesContainer class, this is a class object! And if we did our homework on ES6, we can also state that this is a constructor function. That's why fetchIfNeeded() is undefined - it's defined on GalleriesContainer's prototype.
Now we can create an instance of GalleriesContainer class that will have fetchIfNeeded() method. We can try to access it through prototype, call it - but what will we achieve? fetchIfNeeded() accepts props as an argument and we don't have any. It doesn't return any data, but dispatches an action.
Looks like we will have some thinking and refactoring to do.
We need a method on GalleriesContainer class that will actually return data to us (via promise, of course). We should be able to call it without creating instances or accessing prototypes.
We remember that we can define methods in ES6 classes as static and call them without creating an object of a class. That's a start.
We create new static method on GalleriesContainer. Let's call it fetch. It's important to note that we will still need fetchIfNeeded() method. It will have access to component's props and will continue to perform checks. But we will move dispatching and action to our new fetch method:
static fetch() {
dispatch(fetchGalleriesData());
}
fetchIfNeeded() has access to dispatch from component's props, but our new static method doesn't. We have to pass dispatch() function as an argument:
static fetch(dispatch) {
dispatch(fetchGalleriesData());
}
Let's update fetchIfNeeded() method:
feetchIfNeeded(nextProps) {
if (!this.props.galleries.length && !nextProps.galleries.length) {
this.constructor.fetch(this.props.dispatch);
}
}
Now fetchIfNeeded calls fetch method, passing dispatch function into it.
If you're a bit lost between instances, objects classes, constructor functions - here's a little explanation.
ES6 classes are not real classes like in Java or C++. They are good old objects, syntax sugar on top of constructor functions. So this
class ExampleClass {
exampleMethod() {
console.log('Hello from a method of example class');
}
static staticMethod() {
console.log('Hello from static method of example class');
}
}
is equivalent to this:
function ExampleFunction() {}
ExampleFunction.prototype.exampleMethod = function () {
console.log('Hello from a method of example function');
};
ExampleFunction.staticMethod = function () {
console.log('Hello from static method of example function');
};
We cannot directly access non-static methods without creating an instance of class/constructor function. We cannot directly access static method from an instance. But we can still access static method from an instance through the constructor property.
Constructor property for me for a long time was one of those "WUT?" Javascript's features, but in reality it's nothing special. It's relevant only in the context of constructor functions and prototypal inheritance. It simply points to the constructor function that created this particular instance, so:
let funcInstance = new ExampleFunction();
console.log(funcInstance.constructor === ExampleFunction);
will print true. And thus we can access static method from an instance like this:
funcInstance.constructor.staticMethod();
But fetch() method doesn't return anything, and we need it to give us data on the server, so:
static fetch(dispatch) {
return dispatch(fetchGalleriesData());
}
Now we return results of a dispatch() call. But what are these results? fetchGalleriesData is intercepted in middleware, where we call fetch() function. Right now we're not returning anything from middleware.
But we can return results of fetch() function call inside the middleware. (isomorphic-fetch, don't confuse with HoC fetch() method). It will be returned from dispatch(), so it will be available in fetch() method. Then we just return it from there to the outside world.
Let's update our middleware:
/shared/redux/ dataMiddleware.js:
export default store => next => action => {
...
return fetch(url).then(response => {
...
}).then(data => {
...
return data;
...
});
};
So now in our /server/serve.js we can do this (let's pretend we already have the store):
app.get('*', (req, res) => {
match({ routes, location: req.originalUrl }, (err, redirect, renderProps) => {
...
renderProps.components[1].WrappedComponent.fetch(store.dispatch).then(data => {
console.log(data);
});
});
res.setHeader('Content-Type', 'text/html');
res.send(layout);
});
aaaaaand we get undefined. Now what!?
If look again at flow of our app we will see that DATA_PLEASE action from galleries HoC is dispatched. It's intercepted by middleware and then middleware makes XHR request... Wait, what XHR request? We're on the server!
This means that on server isomorphic-fetch library will still try make HTTP request to the API. It's ineffective, but more importantly - not working! And it's easy to see why:
/shared/redux/dataMiddleware.js:
export default store => next => action => {
...
let url = '/api/' + action.key;
...
};
The URL is relative! It's no problem on the browser - request scheme, host and port are added automatically, but on server we don't have that information. I mean we have it in ExpressJS's middleware, but it's so far away from redux's middleware. Are we supposed to drag the request object through all those function calls? Ugh.
"A good general knows when to retreat". Let's just hardcode http://localhost:3000 for server rendering for now. We’ll attack this problem with fresh forces later.
export default store => next => action => {
...
let url = '/api/' + action.key;
if (typeof window === 'undefined') url = 'http://localhost:3000' + url;
...
};
Now our middleware works both on server and in browser. Time to create a store so we have dispatch() function to pass the fetch() methods:
/server/serve.js
...
import configureStore from '../shared/redux/configureStore.js';
...
app.get('*', (req, res) => {
match({ routes, location: req.originalUrl }, (err, redirect, renderProps) => {
let store = configureStore(req.state);
renderProps.components[1].WrappedComponent.fetch(store.dispatch).then(data => {
console.log(data);
});
});
...
});
...
Note that we pass req.state to configureStore function as initial state - it contains our app state. But what about our page module? It needs a splat to figure out what page to load! Conveniently, it will be accessible to us through renderProps.params field we are on /about or /contacts URLs:
console.log(renderProps.params);
gives us
{ splat: 'about' }
So how do we pass everything that we need to fetch function? We can take renderProps.params object, add dispatch() function from the store to it, and let module figure out the rest.
/server/serve.js
...
app.get('*', (req, res) => {
match({ routes, location: req.originalUrl }, (err, redirect, renderProps) => {
let store = configureStore(req.state);
let { params } = renderProps;
params.dispatch = store.dispatch;
renderProps.components[1].WrappedComponent.fetch(params).then(data => {
});
});
...
});
...
Galleries module doesn't have any params in it's routing. It will get object with only dispatch() function.
Page module needs splat, so it will get object with splat and dispatch. Let's update Page module's HoC:
/shared/modules/page/container.jsx
...
class PageContainer extends React.Component {
componentDidMount() {
this.feetchIfNeeded(this.props);
}
componentWillReceiveProps(props) {
this.feetchIfNeeded(props);
}
feetchIfNeeded(nextProps) {
if ((!this.props.page.title && !nextProps.page.title) || nextProps.params.splat !== this.props.params.splat) {
this.constructor.fetch({
dispatch: this.props.dispatch,
splat: nextProps.params.splat
});
}
}
static fetch({ dispatch, splat }) {
return dispatch(fetchPageData(splat));
}
...
}
...
fetch() method now expects an object with dispatch and splat fields. We provide it both in fetchIfNeeded() method and on server.
Now we update Galleries module's HoC:
/shared/modules/galleries/container.jsx
...
class GalleriesContainer extends React.Component {
...
feetchIfNeeded(nextProps) {
if (!this.props.galleries.length && !nextProps.galleries.length) {
this.constructor.fetch({ dispatch: this.props.dispatch });
}
}
static fetch({ dispatch }) {
return dispatch(fetchGalleriesData());
}
...
}
...
We can now update Blog module accordingly, but Index module is a bit different. It doesn't need any data, so it doesn't need fetch() method. Let's add a fallback to /server/serve.js
...
app.get('*', (req, res) => {
match({ routes, location: req.originalUrl }, (err, redirect, renderProps) => {
let store = configureStore(req.state);
let { params } = renderProps;
params.dispatch = store.dispatch;
let component = renderProps.components[1].WrappedComponent;
(component.fetch ? component.fetch(params) : Promise.resolve()).then(data => {
});
});
...
});
...
Here we first check if component has fetch() method. If it does we call it, if it doesn't we create an empty promise and resolve it. We need a promise to have something to call then() on.
Now we have app state and module's state, time to actually render something!
...
import React from 'react';
import { RouterContext, match } from 'react-router';
import { Provider } from 'react-redux';
import { renderToString } from 'react-dom/server';
...
app.get('*', (req, res) => {
match({ routes, location: req.originalUrl }, (err, redirect, renderProps) => {
let store = configureStore(req.state);
let { params } = renderProps;
params.dispatch = store.dispatch;
let component = renderProps.components[1].WrappedComponent;
(component.fetch ? component.fetch(params) : Promise.resolve()).then(data => {
let content = renderToString(
<Provider store={store}>
<RouterContext {...renderProps} />
</Provider>
);
console.log(content);
res.setHeader('Content-Type', 'text/html');
res.send(layout);
}).catch(err => {
console.log(err);
});
});
});
Here we:
- create a Provider component;
- pass our store into so that all modules and component work just like they do in the browser;
- inside Provider we render RouterContext component. It, React-router documentation says, "renders the component tree for a given router state. Its used by
but also useful for server rendering". We pass this component tree to React's renderToString()function and finally see our HTML!
<div data-reactroot="" data-reactid="1" data-react-checksum="1115628634">
<header data-reactid="2">
<ul data-reactid="3">
<li class="main-menu__item" data-reactid="4"><a href="/" data-reactid="5">Main page</a></li>
...
</ul>
</header>
<div data-reactid="14">
<h1 data-reactid="15">Galleries</h1>
<div data-reactid="16">
<h2 data-reactid="17">First gallery</h2>
<div data-reactid="18">
<img src="http://loremflickr.com/300/300" alt="First image of the first gallery" data-reactid="19"/>
<p data-reactid="20">First image of the first gallery</p>
</div>
...
</div>
...
</div>
</div>
<footer data-reactid="32">
<!-- react-text: 33 -->c <!-- /react-text --><!-- react-text: 34 -->2016<!-- /react-text -->
</footer>
</div>
We see React's footprints - "data-reactroot", "data-reactid", "data-react-checksum". We did it, we render our app on server!
But we're not sending it to the browser, we're still sending contents of /server/layout.html. We need to inject string rendered by React into it.
At this points words like HAML, Jade and Handlebars start flying around - templating engines. We won't use any of them and opt for the best alternative - String.replace(). We will insert string {{content}} into /server/layout.html where the content should go. Then with magical powers of String.replace() we will put our content there:
/server/layout.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Another universal React/React-router/Redux tutorial</title>
</head>
<body>
<div class="container" id="root">{{content}}</div>
<script src="js/main.js"></script>
</body>
</html>
and in /server/serve.js:
app.get('*', (req, res) => {
match({ routes, location: req.originalUrl }, (err, redirect, renderProps) => {
...
let component = renderProps.components[1].WrappedComponent;
(component.fetch ? component.fetch(params) : Promise.resolve()).then(data => {
let content = renderToString(
<Provider store={store}>
<RouterContext {...renderProps} />
</Provider>
);
let response = layout.replace('{{content}}', content);
...
res.setHeader('Content-Type', 'text/html');
res.send(response);
}).catch(err => {
console.log(err);
});
});
});
And now browser receives HTML with our rendered component.
Wait! Server is returning HTML, we can see it if we open source code in the browser. But our app still sends requests for app data and module's data. Also React gives us some warning in the console:
"Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting.""
React understands that it has got server generated HTML, but cannot reuse it and has to start from scratch.
If we google this error, we find this brilliant stackoverflow answer:
This may sound crazily simple, but in your server-side template, wrap your React markup in an extra
<div>.
This actually works! If we replace {{content}} with <div>{{content}}</div> the error goes away. But we don't solve the problem by removing the symptoms.
Let's start with requests to API, that are still being made. We render the content, but it seems that our app cannot create the state from HTML. We have that state on server, maybe we can reuse it in the browser?
What if we will include JSON representation of our state in HTML and then pick it up when React initializes? We can use the power of String.replace() again:
/server/layout.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Another universal React/React-router/Redux tutorial</title>
</head>
<body>
<div class="container" id="root">{{content}}</div>
<script>var __INITIAL_STATE__ = {{state}}</script>
<script src="js/main.js"></script>
</body>
</html>
/server/serve.js
...
app.get('*', (req, res) => {
match({ routes, location: req.originalUrl }, (err, redirect, renderProps) => {
...
(component.fetch ? component.fetch(params) : Promise.resolve()).then(data => {
...
let response = layout.replace('{{content}}', content);
response = response.replace('{{state}}', JSON.stringify(store.getState()));
res.setHeader('Content-Type', 'text/html');
res.send(response);
}).catch(err => {
console.log(err);
});
});
});
...
We get the state on server from store.getState() and insert it into /server/layout.html. We make available to us it the browser via window.__INITIAL_STATE__ variable. Then we have to pass this state to our app in /shared/Root.jsx:
...
let initialState = {};
if (typeof window !== 'undefined' && typeof window.__INITIAL_STATE__ !== 'undefined') {
initialState = window.__INITIAL_STATE__;
}
const store = configureStore(initialState);
...
Here we check if window variable exists and it has __INITIAL_STATE__ field. If we don't do this, we will have an error on the server, were there is no window object.
And that's it! Now our app wires up correctly after loading and doesn't issue unnecessary requests.
We're almost there - all our initial goals are fulfilled. We have one last problem to sort out - that hardcoded URL in the middleware.
You can find all code of this section here: https://github.com/graymur/another-react-universtal-tutorial. Clone the repo, switch to branch "4", run npm install and npm start commands and navigate to http://localhost:3000. You will also need babel-node installed.