JavaScript in Plain English

New JavaScript and Web Development content every day. Follow to join our 3.5M+ monthly readers.

Follow publication

Intro to React Server Side Rendering

How to build a React SSR app without any tooling or framework

Suhan šŸŽƒ Wijaya
JavaScript in Plain English
8 min readJan 11, 2021

Source: Reddit and Dilbert by Scott Adams

This is the first in (hopefully) a series of articles to document the lessons I learned building and maintaining SSR apps.

This article does not cover the pros/cons of SSR, or the pros/cons of not using tooling or frameworks (e.g., Create React App, Next.js, etc).

I fully acknowledge that manually setting up build configs is painful. There is plenty of great tooling out there to abstract away these configs, so you probably donā€™t even need to touch any of this stuff to build awesome apps.

But for those ready to embrace the painā€¦

Overview

Letā€™s start with the basics. We will use React, webpack, and Express to build an SSR app that works as follows:

  1. Browser sends HTTP request to server to load a page.
  2. Server receives HTTP request and turns React JSX into HTML markup.
  3. Server inserts the markup into a HTML template and sends the HTML response back to the browser.
  4. Browser renders the HTML, downloads the client-side JavaScript bundle, and ā€œhydratesā€ the HTML.

Also, I will focus on dev mode to keep this article short. Iā€™m saving production mode for a future(ish šŸ˜…) article.

Project structure

Before diving into the code, letā€™s get situated with the project structure.

.
+-- client
| +-- components
| | +-- App
| | | +-- index.js
| | | +-- style.less
| +-- index.js
+-- server
| +-- index.js
+-- babel.config.js
+-- package.json
+-- webpack.client.config.js
+-- webpack.server.config.js
+-- webpack.shared.config.js

A quick rundown of the files in this project:

  • ./client/components contains React and CSS code.
  • ./client/index.js is the client-side entry point for webpack, where we ā€œhydrateā€ the HTML rendered on the server-side.
  • ./server/index.js is the server-side entry point for webpack, where we define the route to serve the HTML page.
  • ./babel.config.js is the thing that enables you to use React JSX and ES6+ features in the browser and Node.js. You may have seen alternative versions of this file (.babelrc, babel.config.json, etc).
  • ./webpack.shared.config.js is the config that webpack uses for both client-side and server-side code.
  • ./webpack.client.config.js is the config specific to the client-side code.
  • ./webpack.server.config.js is the config specific to the server-side code.

Dependencies

Here are the dependencies (and versions) used at the time of this writing. I will also mention which relevant dependencies to install in each of the following sections.

Now letā€™s look at each file in our project in more detail.

webpack

If you are accustomed to building SPAs (Single Page Apps), you may not have needed webpack to process your server-side code. But to enable SSR, the server must be able to read React code, or specifically ā€” JSX. This means, we now need webpack to work its magic on our server-side code. Plus, youā€™ll also get to use ES6+ syntax that may not be natively supported in Node.js, e.g., import and export. (Sidenote: you donā€™t need webpack if you choose not to write JSX at all. šŸ˜¬)

I wonā€™t go through every webpack option in great detail, but here is a great explainer if you are interested.

Common webpack config for client-side and server-side

Install the relevant dependencies:

npm i webpack webpack-cli babel-loader
webpack.shared.config.js

Annotations of the code comments above:

[A] For our purposes, we want to explicitly set mode to development. If we go with the default value of production, we may lose useful console warnings/errors from libraries like React, and the error stack traces are incredibly hard to read due to code minification. Read more about production and development modes here.

[B] This tells webpack to preprocess .js files with babel-loader, which transpiles ES6+ and JSX code into JavaScript code that is readable by browsers and Node.js servers. This loader uses the options we specify in babel.config.js.

[C] This means I donā€™t have to type out .js or .less when importing files with those extensions. For example, import App from ā€˜./components/Appā€™.

Babel config

Install the relevant dependencies:

npm i babel-loader @babel/core @babel/preset-env @babel/preset-react
babel.config.js

Annotations of the code comments above:

[A] This tells webpack to transpile ES6+ features into JS code thatā€™s natively supported in Node.js and (most modern) browsers. Read the docs for more details.

[B] This tells webpack to transpile React JSX into JavaScript code. Read the docs for more details.

Client-side webpack config

Install the relevant dependencies:

npm i webpack webpack-cli webpack-merge webpack-dev-server mini-css-extract-plugin css-loader less-loader less
webpack.client.config.js

Annotations of the code comments above:

[A] This is the entry point for the client-side code, where we render the React app into the DOM.

[B] This tells webpack to save the transpiled client-side JS bundle output as ./build/client/scripts/bundle.js. Not super important for dev mode because we are using webpack-dev-server to transpile the client-side bundle ā€œin memoryā€. Per the docs:

webpack-dev-server doesnā€™t write any output files after compiling. Instead, it keeps bundle files in memory and serves them as if they were real files mounted at the serverā€™s root path.

[C] The publicPath option tells webpack where we will be serving the client-side bundle. Notice that we are using the same clientPort for devServer, which tells webpack-dev-server to serve the client-side bundle from http://localhost:8080/. And since the filename option tells webpack to nest bundle.js in a scripts folder, the client-side bundle will be served from http://localhost:8080/scripts/bundle.js.

[D] CSS modules and CSS preprocessors (e.g., Less, Sass) deserve an article. But in a nutshell, this piece of config tells webpack to:

  • transpile .less files into CSS code that the browser understands,
  • allow us to import style from ā€˜./style.lessā€™ which is scoped locally to the component importing it (i.e., we donā€™t have to worry about CSS class naming collisions or specificity issues as the app grows),
  • generate a CSS bundle thatā€™s served separately from the JS bundle. In this instance, the MiniCssExtractPlugin tells webpack to serve the CSS bundle from http://localhost:8080/styles/bundle.css in dev mode.

[E] Remember webpack.shared.config.js? This line merges webpack.shared.config.js with webpack.client.config.js.

Server-side webpack config

Hang in there, this is the last webpack config that weā€™ll cover.

Install the relevant dependencies (and grab a ā˜•ļø):

npm i webpack webpack-cli webpack-node-externals css-loader
webpack.server.config.js

Annotations of the code comments above:

[A] The default value is web, so we need to explicitly set it to node for webpack to work its magic on the server-side code.

[B] This is the entry point for the server-side code.

[C] This tells webpack to save the transpiled server-side JS bundle output as ./build/server/bundle.js.

[D] This tells webpack not to include the code from node_modules in the server-side bundle.

[E] This tells webpack not to do any work over the CSS code on the server-side, but simply to make sure that every HTML elementā€™s className matches that in the CSS code being served on the client-side.

[F] Remember webpack.shared.config.js? This line merges webpack.shared.config.js with webpack.server.config.js.

React component

Install the relevant dependencies:

npm i react

Letā€™s create a simple React component App, which renders our favorite greeting with some basic styles, as well as a button that displays an alert dialog when clicked. We will render this component on the server-side and hydrate it on the client-side.

client/components/App/index.js
client/components/App/style.less

Server-side code

Install the relevant dependencies:

npm i express react react-dom

Letā€™s create an Express server and define a route that serves an HTML page when a user visits http://localhost:3000/

server/index.js

Annotations of the code comments above:

[A] This turns the React component App into HTML string, which we then insert in between the div with the ID ā€œssr-appā€.

[B] Remember the devServer option in webpack.client.config.js to start webpack-dev-server in dev mode? These script and link tags tell the browser to fetch the client-side JS and CSS bundles respectively from the webpack-dev-server running on http://localhost:8080.

Client-side code

Install the relevant dependencies:

npm i react react-dom

In the client-side entry point, we will ā€œhydrateā€ the React component that was SSR-ed into the root DOM container with the ID ā€œssr-appā€.

client/index.js

Per the docs:

If you call ReactDOM.hydrate() on a node that already has this server-rendered markup, React will preserve it and only attach event handlers, allowing you to have a very performant first-load experience.

So in this example, the client-side code simply attaches the buttonā€™s click handler without having to re-render any markup in the App component.

Putting it all together

Install the relevant dependencies:

npm i rimraf webpack webpack-cli webpack-dev-server npm-run-all nodemon

This is the scripts key in the package.json file, where we define several npm scripts to build and start the app in dev mode.

package.json

Letā€™s look at each of them:

  • clear ā€” This uses rimraf to delete the ./build folder.
  • build:server ā€” This tells webpack to build the server-side code and save the bundle output to ./build/server/bundle.js (as per ./webpack.server.config.js).
  • start:server ā€” This starts the Express server on http://localhost:3000.
  • dev:server ā€” This uses nodemon to monitor any file changes in the working directory (minus ./build), and npm-run-all to re-run clear, build:server, andstart:server whenever there are file changes.
  • dev:client ā€” This tells webpack to ā€œbuildā€ the client-side code, save the bundle output ā€œin memoryā€, and serve it from http://localhost:8080 (as per ./webpack.client.config.js).
  • dev ā€” This runs all of the above with a single command.

Run npm run dev in the terminal to spin up the SSR app. Open up http://localhost:3000 in your browser. šŸŽ‰šŸ„³

Server-side rendering and client-side hydration

And click on the button Say Hello Back! šŸ™ŒšŸŽŠ

Clicking the button triggers the alert dialog

Now, letā€™s disable client-side JavaScriptā€¦

Disable client-side JavaScript

ā€¦and refresh the page. ITā€™S NOT A BLANK PAGE! šŸ¤Æ

Server-side rendered page

Sidenote: nothing will happen if you click on the button. Thatā€™s because the event handlers are attached by the client-side code (aka ā€œhydrationā€). Recall the docs:

If you call ReactDOM.hydrate() on a node that already has this server-rendered markup, React will preserve it and only attach event handlers, allowing you to have a very performant first-load experience.

And since we disabled client-side JavaScript, what you see is just plain HTML.

Source: CoderPedia

In future(ish šŸ˜…šŸ¤žšŸ») articles, I plan to cover more advanced features like routing, data fetching, caching, code-splitting, lazy-loading, and deploying a production app.

šŸ“« Letā€™s connect on LinkedIn or Twitter!

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Published in JavaScript in Plain English

New JavaScript and Web Development content every day. Follow to join our 3.5M+ monthly readers.

Written by Suhan šŸŽƒ Wijaya

I write the legacy code of tomorrow, today. Letā€™s connect on linkedin.com/in/suhanwijaya

Responses (4)

Write a response