Server-Side Rendering with Redux and React-Router
A.k.a. How we went about rendering for our side project, Refactor.io. At Codementor, we use React to build some of our services. Here’s an article one of our devs wrote on his experiences so far!
Preface
For the past few months, I’ve been using React for a lot of projects. Recently I started to use Redux, and man did it blow me away XD. The source code is nothing short of amazing, and I feel like I’ve learned a lot from it.
At any rate, I’ve used Redux for the following projects:
- A Desktop app (built with Electron)
- A universal web app
- I added Redux to a universal web app that was built with Fluxxor. This way, I can build new features with Redux without having to rewrite everything.
I plan to write a 3-part series about Redux:
- Hello Redux: This article will introduce you to Redux and go through the reasons I think it is awesome
- Server-Side Rendering: This will be a tutorial on how to use Redux and react-router to do server-side rendering
- Unit Testing: I’ll talk about the problems I faced when trying to test Redux code and how I solved them. I’ll also talk about how to make sure your webpack loaders won’t interfere with your tests.
Onto the Main Article!
I’d like to skip the first article and start with server-side rendering, since I think there are already a lot of good existing articles that introduce you to Redux. That, and the fact that I think server-side rendering is more interesting .
Server-side Rendering
But wait! Before I get into anything, let us first understand the problems we’ve faced before the advent of React’s game-changer.
Single Page Applications
As computers & internet browsers became more and more powerful, developers started to move a lot of server-side logic to the client-side (the browser), which eventually led to web applications becoming what is commonly known as single page applications, or SPAs. The star of (front-end) JavaScript changed from jQuery to Backbone.js, AngularJS, and EmberJS to more recently, React.
So what’s the big deal about single page applications? They allow websites to function as independent apps (think of iOS, Android, or desktop apps), which communicate with servers through APIs. This has many advantages. Firstly, for the user, their user experience will be greatly improved as the website won’t have to constantly reload itself. Secondly, the server’s loading will be freed up, as much of the rendering work gets moved to the end-users’ browsers. Finally, development is much easier as the server an client now shares an API, which means that your front-end code won’t be messed up by changes to the database. In other words, now that your front-end app is self-contained, maintenance becomes less of a pain.
However, Single Page Apps are not without pitfalls. It has two major problems:
- Not SEO-friendly
This is because a lot of HTML elements of single page apps are rendered through JavaScript, but currently search engine crawlers are unable to see what gets rendered. So, crawlers that come across SPAs will be seeing a blank HTML body (however, it appears as though this problem has been addressed, so we may be at a turning point). - Slow initial loading time
Single Page Apps would have to wait until the JavaScript gets loaded before the JavaScript renders the HTML. This means users have to wait longer before they can see the website content.
That’s Why We Need Server-Side Rendering!
Let’s split up the application’s architecture into three parts: an API server that provides data, a web server that will share code with the client-side and also render HTML, and finally the client i.e. the code that gets run in the browser.
For more details about this sort of architecture, be sure to check out Airbnb’s blog post.
Image source: Airbnb
Basically, Server-Side Rendering will allow part of your code to be ran on your server first. This means the server will first obtain the data from your API that is needed to render on the initial page’s HTML, and then it will package and send this data to the client.
After the client gets the initial page HTML and the required data, it will continue the whole JavaScript rendering business, but it already has all the required data. So, using the small example above, a client-side rendering SPA would have to start from scratch, but a server-side rendering SPA would be at a starting point where they already have all the data. Thus, this solves the SEO and slow initial loading problems that SPAs share).
This seems like a rather intuitive idea, but it was only taken more seriously when React came out, since React allows you to do server-side rendering in an elegant manner.
To sum up, server-side rendering can be broken down into 3 steps:
- obtain the data needed to render the initial loading page
- render the HTML using this data
- package the HTML and send it to the client side
Ok, Ok. Let’s Talk about Redux Already!
Here’s the link to the Source Code for you to run & follow along with, since I won’t be putting all of my code here.
The Sample App
Let’s say we have a dynamic web page called Question
, and we want to call an API to obtain the current questions
and then render the HTML.
We have:
- a
questions
reducer - a
loadQuestions
action. This will put the data into thequestions
reducer after the question has been successfully fetched from the API.
The Redux app’s entry point would be:
ReactDOM.render((
<Provider store={store}>
<Question />
</Provider>
), document.getElementById('root'));
And the containers/Question.js
would look like:
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { loadQuestions } from 'actions/questions';
import _ from 'lodash';
class Question extends Component {
componentDidMount() {
this.props.loadQuestions();
}
render() {
return (
<p>
<h2>Question</h2>
{
_.map(this.props.questions, (q)=> {
return (
<p key={q.id}> { q.content }</p>
);
})
}
</p>
);
}
}
function mapStateToProps (state) {
return { questions: state.questions };
}
export { Question };
export default connect(mapStateToProps, { loadQuestions })(Question);
The actions/questions.js
would look like:
export const LOADED_QUESTIONS = 'LOADED_QUESTIONS';
export function loadQuestions() {
return function(getState, dispatch) {
request.get('http://localhost:3000/questions')
.end(function(err, res) {
if (!err) {
dispatch({ type: LOADED_QUESTIONS, response: res.body });
}
})
}
}
As you can see from the code snippets above, after a Question
mounts, it will send an API to the server to pull the necessary data and then update the view.
Server-Side Rendering with Redux
The front-end side of the sample Redux app Question
should be quite straightforward, but things get a bit more complex when you want to do server-side rendering.
Upon setting up the server, we won’t have to change any server-side code whenever we add or adjust anything on the front-end. In other words, in a universal app, the server-side setup is decoupled from the business logic.
So, let’s review again what we’re going to do on the server-side:
- Obtain the data needed to render the initial loading page
- Render the HTML using this data
- Package & send the HTML to the client
To do this, we would need to solve two questions:
- When a new request comes in, how do we know which API to call or how do we prepare the application’s state?
- After we call an asynchronous API, when do we know the data is prepared and ready to be sent to the client?
The first issue is actually related to routing. Using the sample Question
component above, the data entry point will be inside componentDidMount()
. In most cases, we need a router to control the relationship between the URL and its respective component. My personal approach is to stick a static method fetchData()
into every routing’s leaf node (if the routing is nested, I’d put it in the innermost one). By doing so, we can use react-router to match the URL to the component we’ll be rendering, and thus calling fetchData()
and obtaining the data entry point.
Moving on, we can use promises solve the second question. So, here we’d make the fetchData()
used above to return a promise, and once this promise is resolved, it would mean that the asynchronous API call has completed and that the data is ready.
That said, the server-side code would thus look something like:
import { RoutingContext, match } from 'react-router'
import createMemoryHistory from 'history/lib/createMemoryHistory';
import Promise from 'bluebird';
import Express from 'express';
let server = new Express();
server.get('*', (req, res)=> {
let history = createMemoryHistory();
let store = configureStore();
let routes = crateRoutes(history);
let location = createLocation(req.url)
match({ routes, location }, (error, redirectLocation, renderProps) => {
if (redirectLocation) {
res.redirect(301, redirectLocation.pathname + redirectLocation.search)
} else if (error) {
res.send(500, error.message)
} else if (renderProps == null) {
res.send(404, 'Not found')
} else {
let [ getCurrentUrl, unsubscribe ] = subscribeUrl();
let reqUrl = location.pathname + location.search;
getReduxPromise().then(()=> {
let reduxState = escape(JSON.stringify(store.getState()));
let html = ReactDOMServer.renderToString(
<Provider store={store}>
{ <RoutingContext {...renderProps}/> }
</Provider>
);
res.render('index', { html, reduxState });
});
function getReduxPromise () {
let { query, params } = renderProps;
let comp = renderProps.components[renderProps.components.length - 1].WrappedComponent;
let promise = comp.fetchData ?
comp.fetchData({ query, params, store, history }) :
Promise.resolve();
return promise;
}
}
});
});
The server’s view template index.ejs
would be:
<!DOCTYPE html>
<html>
<head>
<title>Redux real-world example</title>
</head>
<body>
<p id="root"><%- html %></p>
<script type="text/javascript" charset="utf-8">
window.__REDUX_STATE__ = '<%= reduxState %>';
</script>
<script src="http://localhost:3001/static/bundle.js"></script>
</body>
</html>
I’d add the static method fetchData()
to containers/Question.js
like this:
class Question extends Component {
static fetchData({ store }) {
// return a promise here
}
// ...
}
When we get to this point, we face a third problem.
How do we reuse an action in fetchData()
(loadQuestions()
) and return a promise at the same time?
We can solve this by using a middleware to call an API and return a promise to fetchData()
.
The middleware (middleware/api.js
) would look like:
import { camelizeKeys } from 'humps';
import superAgent from 'superagent';
import Promise from 'bluebird';
import _ from 'lodash';
export const CALL_API = Symbol('CALL_API');
export default store => next => action => {
if ( ! action[CALL_API] ) {
return next(action);
}
let request = action[CALL_API];
let { getState } = store;
let deferred = Promise.defer();
// handle 401 and auth here
let { method, url, successType } = request;
superAgent[method](url)
.end((err, res)=> {
if ( !err ) {
next({
type: successType,
response: res.body
});
if (_.isFunction(request.afterSuccess)) {
request.afterSuccess({ getState });
}
}
deferred.resolve();
});
return deferred.promise;
};
Once we have this, we should make these changes to the original action in actions/questions.js
:
export const LOADED_QUESTIONS = 'LOADED_QUESTIONS';
export function loadQuestions() {
return {
[CALL_API]: {
method: 'get',
url: 'http://localhost:3000/questions',
successType: LOADED_QUESTIONS
}
};
}
And our fetchData()
in containers/Question.js
would become like this:
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { loadQuestions } from 'actions/questions';
import _ from 'lodash';
class Question extends Component {
static fetchData({ store }) {
return store.dispatch(loadQuestions());
}
//...
}
And we’re done. YA!
NOTE: You can run the sample using the source code here. You can access the sample app using the URL http://localhost://3000/q/1/hello
😀 For an example of how we’ve used this code, check out our side project, Refactor.io. It’s a simple tool for developers to share code instantly for refactoring and code review.
Conclusion
After having done some universal rendering for a while, I personally feel the results are pretty good. When loading a web page, I’d go “Wow! This is fast!” However, to achieve this sort of speed, you’d need to spend some time designing and working around things (e.g. all those kickass loaders in your webpack). However, after weighing the trade-offs, I personally find universal rendering quite worthwhile.
This article was originally published in Chinese here and was translated by Codementor's content team. Feel free to leave a comment below if you have any feedback or questions!
it is very useful and easy to understand post…
It appears you’d like to know more about using JavaScript, React, and Redux together. These technologies are commonly used to build modern web applications. Here’s an overview of how they work together: <a herf=”https://sslabs.co.in/javascript-react-redux/”Javascript React Redux=”React Redux”></a>
Great article!
unfortunately not working with react-router-dom (v4)
Yes you’re right. React Router has different approaches regarding “finding the matched components” between v3 and v4. So currently the template supports only v3. 😛