The Pain and the Joy of creating isomorphic apps in ReactJS

WebbyLab
11 min readMar 11, 2019

This post is not a tutorial. There are enough of them on the Internet. It is always more interesting to look at a 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 app in a public repo. Here is the running app and here is its code.

IMPORTANT: This is the 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 a suitable option for our case.

Preface

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 a 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 diagrams on 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 app from scratch, then start with reading this tutorial “Handcrafting an Isomorphic Redux Application (With Love)”. It is really great!

About the app

In WebbyLab we use React from the moment it was open sourced by Facebook almost in every our project. We’ve created a 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:

  1. Content is available without authorization.
  2. It should be indexable by search engines.
  3. It should have social networks sharing features.
  4. It should support different languages.
  5. It should work fast

Writing isomorphic application is the simplest and the most suitable solution in this case.

What parts of your app should be isomorphic?

  1. Isomorphic view
  2. Isomorphic styles
  3. Isomorphic routing
  4. Isomorphic data fetching
  5. Isomorphic configuration
  6. Isomorphic localization

Let’s go one after another.

Isomorphic view (Joy #1)

This is the simplest part. It is simply 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:

Server code:

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.

Isomorphic styles

Usually, tutorials omit this. And this is the first place where you start to feel pain ;).

Pain #1: styles import

We use webpack and usually, we import component specific styles in the 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 example:

Such an 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

  1. Feature stability: 0
  2. Status: “deprecated”
  3. Does not work with babel-node. I am not sure why more investigation required.

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:

  1. Inline styles have no problems with server-side import. They can be saved in json files.
  2. React supports inline styles
  3. Inline styles solve a lot of CSS issues without hacks — React: CSS in JS by vjeux

There are several issues I’ve faced using them:

  1. You should emulate pseudo CSS attributes like :hover, :active, :focus with JavaScript.
  2. You should manage vendor prefixes by your own
  3. You should emulate media queries with JavaScript
  4. You need to merge styles some way. (with css you usually just mention several classes names)

I’ve found a great tool for working with inline styles called Radium. It is a great tool which solves all mentioned issues if you develop SPA.

Pain #2: automatic vendor prefixing based on browser DOM

We’ve switched to Radium but when we run our application in the 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 the 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 a client-side and detect browser by user-agent and insert different prefixes (with inline-style-prefixer) for requests from a different browser on the server. I tried, but that time the solution was not reliable. Maybe now it works better (you can check it on 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 conditional import.

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 a 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:

You can find the whole production config on GitHub.

An 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 extensions that should be ignored by babel in .babelrc

and set an 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.

Pain #3: Material UI uses vendor prefixing based on browser DOM

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 auto-prefixing 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.

Pain #4: Assets loading order

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, a user will not wait until large (it can be several megabytes) bundle.js is loaded. A browser will render HTML immediately.

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:

  1. Load CSS
  2. Load components HTML markup
  3. Load JS

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.

You can find whole production webpack config on GitHub.

Isomorphic routing

Joy #2 — React Router

React router starting from version 1.0.0 works great in an 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 a working example of server-side routing.

Isomorphic data fetching

Joy #3 — Redux

Redux is another library that works great in an isomorphic environment. The main issues with isomorphic apps:

  1. The server processes several requests simultaneously. So, you should have an isolated state for each request. No singleton flux stores in this case.
  2. You should create new stores for each request.
  3. You should dump all stores states at the end of request processing and pass this states to the browser. So, a browser will be able to fill existing flux stores with received state and rerender React tree.

With redux it can be done easily:

  1. Just one store
  2. react-redux uses react context to pass request related store way down by React components tree
  3. Redux store has convenient API for dumping and restoring store state.

Here you can find a working code

Data fetching

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 a separate library for using it in third-party projects.

Here is an example that works both on a server and on a client:

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 a server. fetch is a standard that is already supported by Firefox and Chrome natively.

It is the easy part. The more complex part is not to create API library but to use it in an isomorphic environment.

How a client usually works

  1. React component rendering.
  2. Show loading spinner
  3. Fetch all the component (pgge) dependent data
  4. Update the page (rerender React component with fetched data)

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

  1. Preload all required data for the page
  2. Render the page (with data) to string
  3. Send HTML markup to the client

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 the code will look like:

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 does it work?

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 the static method fetchData on each. One important thing is the 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:

Production version a little bit longer. It passes locale information and has propTypes described.

So, on a server our code looks like:

Here is the whole server app — https://github.com/WebbyLab/itsquiz-wall/blob/master/server/app.js

Isomorphic configuration

The simplest way is 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.

  1. You cannot change config without rebuilding the app. It is not an option for me because I want to use the same build on staging and later on production the only difference is configuration options.
  2. You can have inconsistent config state. Backend sees the new one, but frontend does not see changes because the config is packed into the 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

IMPORTANT: Serializing initialState with JSON.strigify will make your application vulnerable to XSS attacks!!! You should use serialize-javascript instead!

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.

and we use config.js in the following manner in our isomorphic code:

Isomorphic localization

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 a 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).

Statistics

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.

Originally published at blog.webbylab.com.

--

--

WebbyLab

We develop scalable and feature-rich Web and Mobile applications using NodeJs, PHP, React, React Native stack.