Sharing React components between SPA and server rendered views

In this article, we are going to introduce how we use shared react component between SPA(react+mobx+webpack), and a traditional server rendered views(express+pug)

Why using SPA and server rendering together for a web app?

As a Sass app(Cominsoon), we can supply users with the best UX experience by building SPA. On the other hand, we want to optimize our SEO ranking, that is why we are rendering static pages like landing page on the server side.

General steps

The shared react components are header and footer which display different content based on user’s login status, and the general steps are:

  1. Specify an id(e.g. <div id=”header” />) in pug for rendering React component
  2. Webpack compiles React component to JS and CSS
  3. Pug loads the JS and CSS

Detailed steps

using header component for the example

Preparing the React component

For rendering a component, you always need a specific HTML element and let react-dom to render your React component on it. Here is the entry point of header component, let’s call it header.js:

client/header.js
import React from 'react'
import { render } from 'react-dom'
import HeaderContainer from './container/HeaderContainer'
render(
<HeaderContainer />,
document.getElementById('header')
)
client/container/HeaderContainer.js
import React, { Component } from 'react'
import Header from '../pages/Application/Header'
import { userStore } from '../stores'
export default class HeaderContainer extends Component {
componentWillMount () {
// Ping server to check and update user's login status
userStore.ping().catch(() => {})
}
render () {
return (
<Header />
)
}
}
client/pages/Application/Header.js
import React, { Component } from 'react'
import { observer } from 'mobx-react'
import { userStore } from '../stores'
require('./Header.scss')
@observer
export default class Header extends Component {
render () {
// the HTML for header, based on userStore
}
}

The reason why we render HeaderContainer.js instead of Header.js is that we need to let header component know how to check user’s login status. In SPA, Header.js is just a pure react component, and it only takes care of rendering header HTML content.

Webpack configuration

We only keep part of the config for a simple demonstration. The outputs are saved to the public folder `/public/shared/` as static assets, and the WebpackPugManifestPlugin is for generating a pug file which contains the scripts we generated.

You may ask why we have to generate a pug file to contain the scripts compiled by webpack? That is because everytime the script files we generated may named with a different hash based on webpack caching strategy, and we do not want to type the files’ name manually in layout pug file every time the output files updated.

const path = require('path')
const webpack = require('webpack')
const WebpackPugManifestPlugin = require('../../bin/webpack-pug')
module.exports = {
...
entry: {
vendor: ['react', 'react-dom', 'mobx', 'mobx-react'],
header: path.join(__dirname, '../header.js')
},
output: {
filename: '[name].[chunkhash].js',
path: path.resolve(__dirname, '../../public/shared')
},
plugins: [
new WebpackPugManifestPlugin(),
...
],
...
}

the content of pug file (/public/shared/sharedScript.pug) generated by WebpackPugManifestPlugin is the following:

script(type="text/javascript",src="/public/shared/manifest.8a0bd365aad45e8144d2.js")
script(type="text/javascript",src="/public/shared/vendor.3d88e7b92df60c725678.js")
script(type="text/javascript",src="/public/shared/header.15f217e43fcd19fc8bf0.js")

Importing shared JS and CSS into layout pug

For the script above we can find that the files(JS, CSS and pug) are generated into the folder( /public/shared/ ). We need to move the pug file to the pug folder (/server/views/ ) first, and we do it in the Makefile

build-shared:
@echo "Building shared files..."
@rm -rf ./public/shared
@mkdir -p ./public/shared
@NODE_ENV=production $(NODE_BIN)/webpack --config client/webpack/shared.js
@mv public/shared/sharedScript.pug server/views/sharedScript.pug

Now we can import them into the layout pug

server/views/_layout.pug
doctype html
html
head
...
link(href='/public/shared/header.css', rel='stylesheet', type='text/css')
body
div(id='header')
include sharedScript

That is all

Basically, the React header component is going to be rendered by the scripts imported in the _layout.pug. In this way, server rendered views can keep the same Header with the SPA.