Server Side Rendering for React App on Express.js

To write this article, I was prompted by the lack of any more or less complete manual on how to make Server Side Rendering for a React application.



When I ran into this problem, I had 2 options to do this, either with the Next.js framework, or using Express.js .



About 100 hours were spent on Next.js investigate to get it for our ready-made large OTT platform, but there were so many problems that we refused it (I’ll write an article about this again), there was a choice for a small one, Express.js , about which I want to tell.



The full code for the demo in this article is here .



Let's start with the initial task and what we had.



We had at that time:





Tasks:





We have decided on the tasks, let's figure out how to do this.



From the documentation of the reaction, we can find out that for the SSR you can use the renderToString () and hydrate () methods, but what to do next?



renderToString - used to generate HTML on the server of our application.

hydrate - used for universal rendering on the client and on the server.



Data loading



To download data on the server side, we use the redux-connect library, which allows you to download the necessary data before calling the first render, which is what we need. To do this, use hoc asyncConnect. On the server side, it loads data, and when routing it works as componentDidMount.



@asyncConnect([ { key: 'usersFromServer', promise: async ({ store: { dispatch } }) => { await dispatch(getUsersData()); return Promise.resolve(); }, }, ])
      
      





We need to create a redux store on the server side. Everything is as usual, just create in the server.js file.



Also on the server side, using the loadOnServer method from redux-connect, we wait for data preload.



Using renderToString we get the Html of our application with data.



The data that we visited can be retrieved using getState () and added via the <script /> tag to the global window object. From which then on the client we will get the data and set it in the gate.



It all looks like this



 app.get('*', (req, res) => { const url = req.originalUrl || req.url; const history = createMemoryHistory({ initialEntries: [url], }); const store = configureStore(initialState, history); const location = parseUrl(url); const helpers = {}; const indexFile = path.resolve('./build/main.html'); store.runSaga(sagas).toPromise().then(() => { return loadOnServer({ store, location, routes, helpers }) .then(() => { const context = {}; if (context.url) { req.header('Location', context.url); return res.send(302) } const css = new Set(); // CSS for all rendered React components const insertCss = (...styles) => styles.forEach(style => css.add(style._getCss())); const dynamicRoutes = [...routes]; dynamicRoutes[0].routes = [...dynamicRoutes[0].routes, ...StaticRoutesConfig]; const appContent = ReactDOMServer.renderToString( <StyleContext.Provider value={{ insertCss }}> <Provider store={store} key="provider"> <StaticRouter location={location} context={context}> <ReduxAsyncConnect routes={dynamicRoutes} helpers={helpers}/> </StaticRouter> </Provider> </StyleContext.Provider> ); const helmet = Helmet.renderStatic(); fs.readFile(indexFile, 'utf8', (err, data) => { if (err) { console.log('Something went wrong:', err); return res.status(500).send('Oops, better luck next time!'); } data = data.replace('__STYLES__', [...css].join('')); data = data.replace('__LOADER__', ''); data = data.replace('<div id=app></div>', `<div id=app>${appContent}</div>`); data = data.replace('<div id="app"></div>', `<div id="app">${appContent}</div>`); data = data.replace('<title></title>', helmet.title.toString()); data = data.replace('<meta name="description" content=""/>', helmet.meta.toString()); data = data.replace('<script>__INITIAL_DATA__</script>', `<script>window.__INITIAL_DATA__ = ${JSON.stringify(store.getState())};</script>`); return res.send(data); }); }); store.close(); }); });
      
      





2 props are passed to the ReduxAsyncConnect component:



the first is our routes, the second helpers (auxiliary functions), which we want to be accessible throughout the application, some analogue of context.



For server routing, you need to use StaticRouter.



The helmet library is used to add seo meta tags. Each page component has a description with tags.



In order for tags to come immediately from the server, it’s used



 const helmet = Helmet.renderStatic(); helmet.title.toString() helmet.meta.toString()
      
      





The routing had to be rewritten to an array of objects, it looks like this.



 export const StaticRoutesConfig = [ { key: 'usersGender', component: UsersGender, exact: true, path: '/users-gender/:gender', }, { key: 'USERS', component: Users, exact: true, path: '/users', }, { key: 'main', component: Users, exact: true, path: '/', }, { key: 'not-found', component: NotFound, }, ];
      
      





Depending on the url that comes to the server, react router returned the desired data page.



What does the client look like?



Here is the main file of the client part. You can add analytics for search engines and code that must be executed for each page on the client side.



browser / index.js



 import 'babel-polyfill'; import { browserRender } from '../app/app'; browserRender();
      
      





App.js file



 const initialState = !process.env.IS_SERVER ? window.__INITIAL_DATA__ : {}; const history = process.env.IS_SERVER ? createMemoryHistory({ initialEntries: ['/'], }) : createBrowserHistory(); const store = configureStore(initialState, history); if (!process.env.IS_SERVER) { window.store = store; } const insertCss = (...styles) => { // eslint-disable-next-line no-underscore-dangle const removeCss = styles.map(style => style._insertCss()); return () => removeCss.forEach(dispose => dispose()); }; export const browserRender = () => { const dynamicRoutes = [...routes]; dynamicRoutes[0].routes = [...dynamicRoutes[0].routes, ...StaticRoutesConfig]; hydrate( <StyleContext.Provider value={{ insertCss }}> <Provider key="provider" store={store} > <ConnectedRouter history={history}> <ReduxAsyncConnect helpers={{}} routes={dynamicRoutes} /> </ConnectedRouter> </Provider> </StyleContext.Provider>, document.getElementById('app'), ); };
      
      





ConnectedRouter from connected-react-router / immutable is used for routing.



For server rendering, we cannot use r eact-router-dom and properly describe our routing via Switch:



 <Switch> <Route path="/about"> <About /> </Route> <Route path="/users"> <Users /> </Route> <Route path="/"> <Home /> </Route> </Switch>
      
      





Instead, as already mentioned, we have an array with the described routes, and in order for us to add them to the application, we need to use react-router-config :



App / index.js



 import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { Helmet } from 'react-helmet'; import { renderRoutes } from 'react-router-config'; import { getRouterLocation } from './selectors/router'; @connect(state => ({ location: getRouterLocation(state), }), null) export default class App extends Component { static propTypes = { location: PropTypes.shape().isRequired, route: PropTypes.shape().isRequired, }; render() { const { route } = this.props; return ( <div> {renderRoutes(route.routes)} </div> ); } }
      
      





Download carnets, styles and fonts on the server



For styles, isomorphic-style-loader was used , since the usual slyle-loader does not work in the webpack with target: “node”;



He adds all the styles to the DOM, thus the ready-made, beautiful page comes from the server. You can save styles in a separate file, so they can be cached by the browser.



To display images and download fonts on the server, webpack loader base64-inline-loader was used .



 { test: /\.(jpe?g|png|ttf|eot|otf|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/, use: 'base64-inline-loader?limit=1000&name=[name].[ext]', },
      
      





It was included for images and all types of fonts that we used. As a result, we received a code from the base64 server that displayed a page with fonts and images without subsequent loading.



For the client build, the usual url-loader and file-loader were used .



 { test: /\.(eot|svg|otf|ttf|woff|woff2)$/, use: 'file-loader', }, { test: /\.(mp4|webm|png|gif)$/, use: { loader: 'url-loader', options: { limit: 10000, }, }, },
      
      





Yes, this increased the size of the loaded page, but it was not very noticeable to the user in terms of download speed.



Dynamic import on server



React.js uses React.lazy and React.suspense to dynamically import and display the loader, but they do not work for SSR.



We used react-loadable , which does the same.



 import Loadable from 'react-loadable'; import Loader from './Loader'; const LoadableComponent = Loadable({ loader: () => import('./my-component'), loading: Loader, }); export default class App extends React.Component { render() { return <LoadableComponent/>; } }
      
      





On the client, this code displays the loader, on the server, in order for the modules to load, you need to add the following code:



 Loadable.preloadAll().then(() => { app.listen(PORT, () => { console.log(` Server is listening on port ${PORT}`); }); });
      
      





Loadable.preloadAll () - returns a promise that says the modules are loaded.



All highlights are resolved.



I made a mini demo using everything that was described in the article .



All Articles