React Server Side Rendering(SSR) for existing big nodejs project. webpack 4

Chen
5 min readMar 25, 2019

--

This article shows the steps/gotchas for switching to SSR for our influencer search engine SocialBook. The assumption is that you are 200% familiar with node/react/redux, and you have built some production grade product used by by millions of users.

Just kidding:)

Note that you can easily use babel-register to run your backend node code, but babel-register is NOT for production. In case you still want to use babel-register, below is a working version of the config. (no .babelrc!!!!, many people on the internet will recommend using a .babelrc under your project root. But I do not want .babelrc to interfere with existing babel)

Those require.extensions are super important as you do not want your server side to handle those non js file. require(‘@babel/polyfill’); is also important for babel to handle async/await stuff.

Once you have the babelRegister file, have another server.js wrapping your original app.js like so:

require(‘./babelRegister’)
require(‘./app’);

Then your backend app.js should just be running fine.

Since babel-register is not meant for production, we have to use webpack to compile the backend code first.

  1. Have a separate ssr webpack config(webpack.ssr.js), the entry point is your original app.js . (as shown above, we were running node app.js )

2. In your package.json, add a command under your scripts:

“build:dev”: “webpack — config ./webpack.ssr.js”,

Now in your terminal, run npm start build:dev and…

start seeing errors

A big headache for us is that there seems to be a bug for sass-loader. We have sass file importing another file with the same name as another javascript file, in our case, it’s _glogal.scss and the other javascript file is global.js. sass-loader will actually use global.js instead of global.scss(weird) and apparently it will fail. So I have to rename/get rid of global.js.

But things are not over yet. there is still something under /node_modules/global/window.js that conflicts with global.sass! So we basically need to replace global.sass with another name… Later we found that there is a better way: using alias to force webpack to look for .scss file instead of js file:

UPDATE: we can use ignore-loader to let server not build css files at all. Then we need the css files to be in index.html in order for webpage to render nicely.

Here is a copy of our SSR webpack:

At this point, when running “npm run build:dev”, an app.compiled.js will be generated. Important notes on the config file:

  1. Need to have target: ‘node’,
  2. Need to set node: { __dirname: true, __filename: true }. Otherwise it will screw all your path.
  3. Ignore css loader: { test: /\.(css|scss)$/, use: [‘ignore-loader’] }, You anyways do not want server side to build your css files. Besides, you will see lots of errors as well.
  4. Add ‘babel-polyfill’ in your entry: [‘babel-polyfill’, path.resolve(__dirname, ‘app.original.js’)],
  5. If you do not want webpack to mess with your NODE_ENV environment, you can add nodeEnv: false, under optimization tag.

Since now we are also building front end react node from backend, its important to know that browser variables like window, document etc are not available to node environment. So when you are writing an isomorphic app, make sure to only use those variables when browser environment is ready. (componentDidMount). Otherwise you will see lots of errors like window/document/navigator not defined.

For example, in our case, sometimes we have to write customized code for mobile environment, and there are lots of places on react component using browser to check whether it is mobile or not. In our server code, we always first check whether user is logged in or not, so we pass back a variable to reducer: device_type indicating whether the request is initiated from mobile. Then on the front end, we check that flag first before checking browser information.

Now we got SSR running, and we can start having customized title for each page. Pretty cool, right? But what if the title need to read from an API call? Also in our case, for each route on the UI, we will first check whether user is authenticated, and because of the asynchronous nature of API calls, front end need to check request_start/success/fail to show different UI status( For example, showing loading indicator when request is made) Can this part be moved into SSR as well? so whenever front end renders page, it already has the information of whether this user has logged in not. Below is how we solve this problem:

Dispatch actions from back end

We thought about just calling internal functions directly from backend. But then we have to assembly the result to the state format that is compatible with the store. For a big project like ours, there are lots of complicated logic in the reducer to post process the result of API calls, so its better/much easier for us to dispatch the actions directly from server side. Lets look at a simple example. In our declarative route (react route 4, guys swithcing to react router 4!!!), we have

{
path: ‘/influencer/:slug’,
exact: true,
component: InfluencerProfilePage,
handler: ‘core_user’,
reducer: influencerActions.getUserProfile,
getTitle: influencerActions.getTitle
},
  1. We specify a backend handler called “core_user”. In our case, it corresponds to a backend class.
  2. We specify a reducer function. In this case it’s influencerActions.getUserProfile.
export function getUserProfile(data, channelType = ‘youtube’, baseUrl = ‘’, headers = ‘’)

Please note that we add a baseUrl and Headers function, because API calls need to pass cookies from header for authentication. Also since backend is calling APIs, it’s missing the context of root API base. so we have to pass a baseUrl to the action.

3. In our backend, each handler has to have a function called getInitialParams. It basically gets the required data back to the reducer.

exports.getInitialParams = function(req, route) {
return [{ slug: req.params.slug }, route.channel, config.base_url.socialbook, req.headers];
}

We pass the base url and req.headers. Again we need to pass cookie back from header for authentication.

4. Now in SSR related code, we have logic like this:

if (route.handler) {
let handler = handlerMap[route.handler];
if (handler && route.reducer) {
let initParams = handler.getInitialParams(req, route);
await store.dispatch(route.reducer(…initParams));
}
}

Because webpack wont work well with dynamic “require”, we have to have a handlerMap. It is a mapping of name to actual class, in our case its:

const handlerMap = {
core_user: require(‘../../share/routes/core_user’)
};

We simply calling handler.getInitialParams first, then using store to dispatch the action.

5. after the action dispatch, we then get the state from store:

let initialState = store.getState();

then we call getTitle (defined in the declarative route) to get the actual title from state

if (route.getTitle) {
seo.title = route.getTitle(initialState, route);
}

6. For all the actions called from SSR, make sure on the front end they are not called again!!! ( I am a big hater of calling same API multiple times).

Through this way, you get a generic way of calling reducer actions from backend.

If in your reducer you are using browser related variables like window, document etc, you need to fix them as well.(refer to device_type i talked about above).

Stay tuned

--

--

Chen
Chen

Written by Chen

A typical engineer turned entrepreneur

No responses yet