This post is not a tutorial. There are enough of them on the Internet. It is always more interesting to look at real production app. A lot of tutorials and boilerplates show how to write isomorphic ReactJS applications, but they do not cover a lot of real-life problems we’ve faced in production. So we decided to share our experience at github and develop the isomorphic react app in a public repo. Here is the running app and here is its code.
IMPORTANT: This is real-world application, so it is always evolving. Our goal is to make working product within a limited amount of time. Sometimes, we have no time for finding the best possible solution, but use just suitable option for our case.
If you have questions about the implementation of your own isomorphic application in React or you have the desire to create it from scratch, you can always contact us and we will try to help you.
Let’s start with the term “Isomorphic application”. An isomorphic JavaScript application is just a JavaScript application that can run both client-side and server-side (in most cases it is a single-page application that can be run on server). I do not like the word “isomorphic”, I believe that programmers should struggle complexity (not only in code) and the word “isomorphic” complicates understanding, therefore it increases complexity :). There is another name for isomorphic JavaScript – “Universal JavaScript”, but in my opinion the word “Universal” is too general. So, in this post, I will use the word “isomorphic” ( even if do not like it 🙂 ).
How do people see an isomorphic app? You can find diagramms in the internet like this one:
But ideally it should look a little bit different:
I mean, not less than 90% of your code should be reused. In large apps it can be even more than 95%.
If you want to develop an isomorphic React app from scratch, then start with reading this tutorial “Handcrafting an Isomorphic Redux Application (With Love)”. It is really great!
In WebbyLab we use React from the moment it was open sourced by Facebook almost in every our project (by the way, we recently wrote about Facebook login in react native). We’ve created keynote clone, excel clone, a lot of hybrid mobile applications, UI for a social monitoring system, comment moderation system, ticket booking system, a lot of admin UIs and more. Sometimes we make isomorphic apps too. The fundamental difference between regular SPA and isomorphic SPA is that in isomorphic SPA you will process several requests simultaneously, therefore you should somehow deal with a global user-dependent state (like current language, flux stores state etc).
itsquiz.com is one of our projects written in ReactJS. itsquiz.com is a cloud testing platform with a lot of amazing features. And one of the key features of the product is a public quizzes catalogue (aka “quizwall”), any user can publish his own tests there and pass others’. For example, you can go there and test your knowledge of ReactJS.
You can watch 1 minute promo video to better understand the idea of the product:
Here are key requirements to Quizwall:
Writing an isomorphic React application is the simplest and the most suitable solution in this case.
By the way, did you know that you can even create AI chatbots using Javascript? We recently wrote our own guide on how we managed to do this.
Let’s go one after another.
This is the simplest part. It is simple because Facebook developers solved this problem already in ReactJS. The only thing we should do is to take React Js library and use it according to documentation.
Client code:
import ReactDOM from 'react-dom'; import App from './App'; ReactDOM.render( <App />, document.getElementById('react-view') );
Server code:
import ReactDOM from 'react-dom/server'; import App from './App'; const componentHTML = ReactDOM.renderToString( <App /> ); const html = ` <html> <head> <title>Quiz Wall</title> </head> <body> <div id="react-view">${componentHTML}</div> </body> </html> `; res.end(html)
As you see, we just use “ReactDOM.renderToString” instead of “ReactDOM.render”. That’s it. Nothing complex and you can find this in any tutorial.
Usually tutorials omit this. And this is the first place where you start to feel pain ;).
We use webpack and usually we import component specific styles in component itself. For example, if we have a component named “Footer.jsx” then we will have less file named “Footer.less” in the same folder. And “Footer.jsx” will import “Footer.less”. The component will have class by its name (“Footer”) and all styles will be namespaced by this class.
Here is a small isomorphic React example:
import React from 'react'; import './Footer.less'; export default class Footer extends React.Component { render() { return ( <div classname="Footer"> <small> Developed by <a href="http://webbylab.com" target="_blank" rel="noopener"> WebbyLab </a> </small> </div> ); } }
Such approach makes our code more modular. Moreover, if we import a component it will automatically import its dependencies (js libs, styles, other assets). Webpack is responsible for handling all file types. So, we have self-contained components.
This approach works great with webpack. But it will not work in pure nodejs, because you cannot import “less” files. So I’ve started looking for a solution.
The first possible one was require.extensions but
So, I’ve started looking for another solution. The simplest one was using inline styles.
Inline styles .
I’ve decided to try inline styles because:
There are several issues I’ve faced using them:
I’ve found a great tool for working with inline styles called Radium. It is great tool which solves all mentioned issues if you develop SPA.
We’ve switched to Radium but when we run our application in isomorphic mode we received strange warnings.
“React injected new markup to compensate which works but you have lost many of the benefits of server rendering.” No, I want all the benefits of server rendering. We run the same code on a server and a client, so why react generates different markup? The problem is with Radium automatic vendor prefixing. Radium creates DOM element to detect list of css properties that should have vendor prefixes.
Here is the issue on Github “Prefixing breaks server rendering”. Yes, there is a solution for it now: using Radium’s autoprefixer on client side and detect browser by user-agent and insert different prefixes (with inline-style-prefixer) for requests from different browser on server. I tried, but that time the solution was not reliable. Maybe now it works better (you can check it by your own :)).
The second problem is that you cannot use media queries. Your server does not have any information about your browser window size, resolution, orientation etc. Here is a related issue https://github.com/FormidableLabs/radium/issues/53.
Solution that works
I’ve decided to switch back to less and BEM but with condititional import.
import React from 'react'; if ( process.env.BROWSER ) { require('./Footer.less'); } export default class Footer extends React.Component { render() { return ( <div classname="Footer"> </div> ); } }
You see that we are using “require” instead of “import” to make it runtime dependendant, so nodejs will not require it when you run code on server.
One more thing we need to do is to define process.env.BROWSER in our webpack config. It can be done in the following way:
var webpack = require('webpack'); module.exports = { entry: "./client/app.js", plugins: [ new webpack.DefinePlugin({ "process.env": { BROWSER: JSON.stringify(true) } }), new ExtractTextPlugin("[name].css") ], // ... };
You can find whole production config on github.
Alternative solution is to create a plugin for babel that will just return “{}” on the server. I am not sure that it possible to do. If you can create babel-stub-plugin – it will be awesome.
UPDATE: We’ve switched to the alternative solution after migrating to Babel 6
We use babel-plugin-transform-require-ignore plugin for Babel 6. Special thanks to @morlay (Morlay Null) for the plugin.
All you need is to configure file extentions that should be ignored by babel in .babelrc
"env": { "node": { "plugins": [ [ "babel-plugin-transform-require-ignore", { "extensions": [".less", ".css"] } ] ] } }
and set environment variable BABEL_ENV=’node’ before starting your app. So, you can start your app like that cross-env BABEL_ENV=’node’ nodemon server/runner.js.
Ok. Let’s go further. We managed our styles. And it seems that the problems with styles are solved. We use Material UI components library for our UI and we like it. But the problem with it is that it uses the same approach to vendor autoprefixing as Radium.
So we had to switch to the Material Design Lite. We use react-mdl wrapper for React.
Great, it seems that we definitely solved all of the problems related to styling… sorry, not this time.
Webpack will generate a javascript bundle for you and will pack all css files to the same bundle. It is not a problem in SPA – you just load the bundle and start your app. With isomorphic SPA everything is not so obvious.
Firstly, it is a good idea to move your bundle.js to the end of markup. In this case, user will not wait untill large (it can be several megabytes) bundle.js is loaded. A browser will render HTML immediately.
<html> <head> <title>Quiz Wall</title> </head> <body> <div id="react-view">${componentHTML}</div> <script type="application/javascript" src="/build/bundle.js"></script> </body> </html>
This works. But moving the bundle.js to the end also moves styles to the end (as they are packed into the same bundle). So, a browser will render markup without CSS and after that it will load bundle.js (with CSS in it) and only after that it will apply styles. In this case, you will get blinking UI.
Therefore, the right way is to split your bundle into two parts and load everything in the following order:
<html> <head> <title>Quiz Wall</title> <link rel="stylesheet" href="/build/bundle.css"> </head> <body> <div id="react-view">${componentHTML}</div> <script type="application/javascript" src="/build/bundle.js"></script> </body> </html>
And the cool thing about webpack is that it has a lot of plugins and loaders. We use extract-text-webpack-plugin to extract css to a separate bundle.
Your config will look similar to this one.
var webpack = require('webpack'); var ExtractTextPlugin = require("extract-text-webpack-plugin"); module.exports = { entry: "./client/app.js", plugins: [ new ExtractTextPlugin("[name].css") ], output: { path: __dirname + '/public/build/', filename: "bundle.js", publicPath: "build/" }, module: { loaders: [ { test: /\.less$/, loader: ExtractTextPlugin.extract("style-loader", "css-loader!autoprefixer-loader!less-loader") } ] } };
You can find whole production webpack config on github.
React router starting from version 1.0.0 works greatly in isomorphic environment. But there are a lot of outdated tutorials written when react-router-1.0.0 was still in beta. Don’t worry, official documentation has working example of server-side routing.
Redux is another library that works greatly in isomorphic environment. The main issues with isomorphic apps:
With redux it can be done easily:
Here you can find working code
You should write code that works both on the server and on the client. Usually in SPAs (even not isomorphic) we write API layer which can be used on the server too. This layer is responsible for all communications with the REST API. It can be packed as separate library for using it in third-party projects.
Here is an isomorphic React example that works both on the server and on the client:
'use strict'; import apiFactory from './api'; const api = apiFactory({ apiPrefix: 'http://itsquiz.com/api/v1' }); // Can be use in a following manner const promise = api.users.list();
For making http request you can use something like axios but I prefer isomorphic-fetch which uses whatwg-fetch (from Github) in browser or node-fetch on server. “fetch” is a standard that is already supported by firefox and chrome natively.
It is the easy part. More complex part is not to create api library but to use it in isomorphic environment.
How client usually works
So, the idea is simple. A user will wait for data but will not wait for UI response. So, you should render immediately without data and show spinner and show the data when it was fetched.
How server usually works
We want to write the same code for two scenarios. How do we handle this? The idea is simple. We use action creators and they trigger data fetching. So, we should describe all page dependencies – action creators that will be used for data fetching.
The isomorphic part of code will look like:
'use strict'; import React from 'react'; import { connect } from 'react-redux'; import { loadActivations } from 'actions/activations'; import connectDataFetchers from 'lib/connectDataFetchers.jsx'; import ActivationsPage from 'components/pages/ActivationsPage.jsx'; class ActivationsPageContainer extends React.Component { render() { return ( <ActivationsPage activations = {this.props.activations} search = {this.props.search} onItemClick = {this.handleQuizCardClick} onSearch = {this.handleSearch} /> ); } } export default connect({activations : state.activations})( connectDataFetchers(ActivationsPageContainer, [ loadActivations ]) );
So, you should wrap your component in another one which will be responsible for data fetching. We use “connectDataFetchers” function for it. It takes React component class and array of references to action creators.
How it works?
In browser the wrapper component will call action creators in componentDidMount lifecycle hook. So, it is a common pattern for SPA.
IMPORTANT: componentWillMount is not suitable for this because it will be invoked on the client and server. componentDidMount will be invoked only on the client.
On the server we do this in a different way. We have a function “fetchComponentsData” which takes an array of components you are going to render and calls static method “fetchData” on each. One important thing is a usage of promises. We use promises to postpone rendering until the required data is fetched and saved to the redux store.
“connectDataFetchers” is extremely simple:
import React from 'react'; let IS_FIRST_MOUNT_AFTER_LOAD = true; export default function connectDataFetchers(Component, actionCreators) { return class DataFetchersWrapper extends React.Component { static fetchData({ dispatch, params = {}, query = {} }) { return Promise.all( actionCreators.map(actionCreator => dispatch(actionCreator({ params, query }))) ); } componentDidMount() { // If the component is mounted first time // do no fetch data as it is already fetched on the server. if (!IS_FIRST_MOUNT_AFTER_LOAD) { this._fetchDataOnClient(); } IS_FIRST_MOUNT_AFTER_LOAD = false; } componentDidUpdate(prevProps) { // Refetch data if url was changed but the component is the same const { location } = this.props; const { location: prevLocation } = prevProps; const isUrlChanged = (location.pathname !== prevLocation.pathname) || (location.search !== prevLocation.search); if (isUrlChanged) { this._fetchDataOnClient(); } } _fetchDataOnClient() { DataFetchersWrapper.fetchData({ dispatch : this.props.dispatch, params : this.props.params, query : this.props.location.query }); } render() { return ( <Component {...this.props} /> ); } }; }
Production version a little bit longer. It passes locale information and has propTypes described.
So, on server our code looks like:
// On every request const store = configureStore(); match({ routes, location: req.url }, (error, redirectLocation, {components, params, location}) => { fetchComponentsData(store.dispatch, components, params, location.query).then(() => { const componentHTML = ReactDOM.renderToString( <Provider store={store}> <RoutingContext {...renderProps}/> </Provider> ); return renderHTML({ componentHTML, initialState: store.getState() }); }); });
Here is the whole server app – https://github.com/WebbyLab/itsquiz-wall/blob/master/server/app.js
The simplest way it to use “config.json” and require it wherever needed. And you can find that a lot of people are doing so. In my opinion, this is a bad solution for isomorphic SPA.
What wrong with it?
The problem is that when you require “config.json” webpack will pack it into your bundle.
The solution is to leave config outside the bundle and place it in some sort of global variable that can be set in index.html.
We load out config on the server and return it in index.html
<div id="react-view">${componentHTML}</div> <script type="application/javascript"> window.__CONFIG__ = ${serializeJs(config, { isJSON: true })}; window.__INITIAL_STATE__ = ${serializeJs(initialState, { isJSON: true })}; </script> <script type="application/javascript" src="${config.staticUrl}/static/build/main.js"></script>
But depending on a global variable in your code is not a good idea. So, we create “config.js” module that just exports global variable. And our code depends on “config.js” module. Our “config.js” should be isomorphic, so on the server we just require json file.
// config.js if (process.env.BROWSER) { module.exports = window.__CONFIG__; } else { module.exports = require('../etc/client-config.json'); }
and we use the “config.js” in the following manner in our isomorphic code:
'use strict'; import config from './config'; import apiFactory from './api'; export default apiFactory({ apiPrefix: config.apiPrefix });
Very few tutorials explain how to deal with localization in a regular SPA. No tutorials at all say how to deal with localization in an isomorphic environment. In general, it is not an issue for most of the developers because there is no need to support other languages except English. But it is really important topic, so I’ve decided to describe localization issues in the separate post. It will be end to end React applications localization guide (including isomorphic issues).
Universal (isomorphic) code – 2396 SLOC (93.3%) Client specific code – 33 SLOC (1.2%) Server specific code – 139 SLOC (5.4%)
While all codebase growths, isomorphic part of the code growths the most. So, code reuse rate will become higher with time.
Learn more about how we engage and what our experts can do for your business
Written by:
Senior Software Engineer at Google Non-Executive Director and co-founder at WebbyLab.
More than 15 years of experience in the field of information technology. Did more than 40 talks at technical conferences. Strong back-end and fronted development background. Experience working with open-source projects and large codebases.
In this article we would like to talk about our experience of creating an AI chatbot with JavaScript and ChatScript. The main task was to…
“Flux Standard Action” has 3700+ stars on github and used by “redux-promise”, “redux-actions” and other libraries. Several weeks ago one of my developers tried to…
The use of microservices architecture is already widespread among global companies. For instance, Amazon, Coca-Cola, and Netflix started out as monolithic apps but have evolved…
Introduction CLI stands for the command-line interface. It’s a common way to interact with different processes. Tools with CLI are often used in software development,…
Introduction WebbyLab’s customer was interested in automation of advertisement banners customisation based on the prepared templates. Basically, you have an index.html with fields where you…
By this article, I’d like to provide you a checklist that will help you to test your product better and discover it more in-depth. Things…