React Isomorphic/Universal App w/NodeJS, Redux, & React Router V4

Summary
When it comes to Single Page Applications SEO becomes a common annoyance. At Dermveda we are eagerly trying to improve our SEO but not retract the user experience by solely doing server-side rendering. Of course we can use a prerendering service such as prerender.io but we have found this to bring forth a mess of complications. For those looking to implement an Isomorphic or Universal React app, we found that there is no ‘defacto’ standard for this process. Companies such as Ebay and Airbnb have blazed the trail by writing awesome blogs on these processes but we decided to do a write up on our own experience using React Router V4 and Redux. The requirements for this application include NodeJS, React, Redux, and React Router V4. Feel free to download this project from Github.
Our Strategy
Our strategy was not necessarily to render everything on each request the browser sent up. We much rather preferred to let our developers write a static fetchData() that would include all Redux actions that would need to run to insure all required data was fetched on the server on first request. This would let the developers write additional async functions in componentDidMount() and other React lifecycle functions that may not be directly related to the important SEO content on the page. Once fetchData() was completed we would have our new initialState and we could render our component on the server. After rendering we deliver our app to the front-end along with the initialState. Finally, once the browser loaded the response (displaying our beautiful server-rendered code) we load our real client-side SPA in the background.
The Problem with SPA’s
SPA’s are awesome. They tend to replace their older server rendered ancestors and have changed the way we write web applications. They are fun to use and usually quick and efficient, however these applications bring all sorts of SEO difficulties to the playing field. Google has stated that they can now read SPA’s but our experience has been that this is not always true and other meta-data crawlers like Facebook and Twitter will not wait for your asynchronous calls to finish. Popular solutions to help this problem are ‘prerendering’ pages. ‘Prerendering’ is usually the process of using a headless browser to load your webpage until everything finishes loading and then taking a snapshot of the html to deliver to to any crawlers. Services like Prerender.io are preferred services and give many caching options to get around the slow ‘prerendering’ process. ‘Prerendering’ has always felt like such a hack and honestly it has always given us unpredictable results and things like hard 404’s have been a bit of a challenge. Not to mention services like Prerender.io use PhantomJS which I have found is comparative to Safari 7 and have proven to be very challenging to debug. So what can we do?! Isomorphic or Universal app design! We server render each request that comes in (waiting for any async requests to finish), deliver content to the user with this new initial state, then load our SPA in the background. Any further navigation from the user upon first load will be using our SPA and all the things we love about SPA’s without the SEO complications.

Getting Started
Let’s get this show on the road. To get started you should clone my repo on github. To allow me to focus on the Isomorphic aspects of this design you need to be well versed in ReactJS, Redux, Webpack, and Express. You will also need to have a HTTP client that can function both in the browser and on NodeJS, we used AXIOS but you could also use something like isomorphic-fetch. Once cloned run:
npm install && npm start
This will show you a working project that you can play around with or follow along with this guide.
Webpack
It is important to note that we used Webpack for this build for both the server files as well as the front-end files. It is important to understand that NodeJS uses a CommonJS environment and does not know what to do with the ES6 ‘Import’ in react. Not to mention any JSX syntax in our NodeJS application will need to be run through Babel to convert it to vanilla javascript. In my project you will notice two Webpack config files to handle this process.
HTTP Client
As mentioned above, AXIOS is our HTTP client and let’s use the same client to make all API calls. This is important to ensure you actually fetch the data you need! Side note: In the future we will be moving to isomorphic-fetch as I have recently run into Service Worker support issues with XMLHttpRequest.
Let’s Write the Router
One of the common annoyances of this Isomorphic design is the router. We need a server and browser compatible router using the latest React-Router V4 without having to maintain two separate routes. We also wanted to use matchPath() in react-router so that our routing logic shared the same base functionality. This makes our router predictable and therefore reliable! Our solution was to create a running array of our router and build our routes at runtime. This let us test server side routes against our array and also build our client-side router from the same array.
Build Client Wrapper
To ensure we have a SPA to deliver to the user we will need to make sure we have an entry point for it to mount to the DOM. This should look familiar and I prefer to call it a ‘wrapper’ because it should function as a wrapper of our application that only the client will use. You will notice I only Import the App entry point and create a new Store and Provider instance. This is a must because our Store is different for the client than the server. Our Store for the client looks for an already initialized state on load. We will see how to send this in the server logic.
Build Server Wrapper
Our server wrapper functions a bit different than our client. It includes our route listener which creates store, fetches data, and renders our component. After rendering our component we pass the string markup to our render function along with our new redux store. Many people like to use templating libraries like Handlebars or Jade but we decided to keep it simple with String literals.
component.fetchData()
You may have also noticed us calling the fetchData() function that returns a promise on our found component. This static function needs to in every container you have in your routes. On our components that need to dispatch redux actions and grab async data we pass the Store and call our actions.
static fetchData({ store }) { return store.dispatch(actions.getName(1));}
Some pages that did not need to retrieve async data would just need to return resolved promises. See example below:
static fetchData({ store }) { return new Promise(resolve => resolve());}
There are many ways around this but this was our preferred method.
Render Page
The rendering of our page is done in renderFullPage() where we pass the html and new initialState for the browser to pick up where we left off.
Redirects
One thing you will notice about the code above is that we have a check of the ‘context.url’ property. This is the best practice according to react-router v4’s documentation for handling redirects. StaticRouter is unable to follow Redirects on the server so this property lets us know if we hit one. You will notice we simply decided to fire a redirect to the new url with corresponding status code. Test ‘/people’ in the browser for how a redirect works.
Helmet
I added react-helmet into this example for an added bonus. This library works well for rendering meta tags for your webpage and I have given an example on how to render it on the server.
Run
Now it is time to run our application. Run npm start and both client and server will build and then checkout http://localhost:9000 for our Isomorphic design web application.
npm start
Testing
Testing this application is easy. Turn off javascript in Chrome and refresh the page!
Improvements
These examples have plenty of room for improvements but I hope these examples helps most of you get started with converting your React and Redux applications to Isomorphic design. Cheers!
Special thanks to Gage Langdon for working with me on this project!