Create React App From Scratch With Isomorphic Rendering
Tags: react, serverrendering, webpack, babel
Published: 2023-09-14 11:03:52

---
title: Create React App From Scratch With Isomorphic Rendering
published: true
description: Using webpack to create a react app with server-side rendering.
tags: #react #serverrendering #webpack #babel
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-07-22 03:47 +0000
---
I'd been always curious about how to manually configure a React project from scratch using webpack. Let me share my learnings with you in this post. 

**The benefits of learning configuring your own project:**
1. Prodviding you a bare bone to start with when create-react-app is an overkill because it includes many redundant functionalities.
2. Learning about how babel transpiles your project, how it works with typescript during development and how to auto build your project along with changes.
3. Mastering webpack configurations to build your own library in the future.

## Isomorphic rendering/Server-side rendering
SSR can greatly improve user's first interaction experience by optimizing the first page load time. It also provides a better search engine experience where the crawl bot can access more meaningful content in your pre-generated html file whereas a pure client-side rendering page only contains `<div id="root"></div>`.

## Project Structure
After building our project, we are expecting a `dist/` folder containing `client/` and `server/` where `client/` contains the code for hydrating the pre-generated content and `server/` contains the server code with NodeJS. Hence, we need two `webpack.config.js` for configuring each directory respectively. Later on, we can boost the server by running `node dist/server/main.js` and see the page result by accessing the localhost url. Here is the preview:
```
>> dist
>>>> client
>>>> server
>> node_modules
>> server
>>>> ...
>>>> webpack.config.js (builds dist/server)
>> src (components)
>> client.tsx (clients-side entry file)
>> webpack.config.js (builds dist/client)
```
### Client side logic
The code below tells React to compare the pre-generated content from server to the component `App` we want to render. Any mismatch is not allowed because SSR provides users an illusion of faster page load time by showing users content generated from server, then attaches React to it. Any mismatch between the content will break that illusion.
```js
import { hydrateRoot } from 'react-dom/client';
import { App } from './src/app';
import React from 'react';

hydrateRoot(document.querySelector('#root')!, <App />);
```
In the `webpack.config.js`, we have `HtmlWebpackPlugin` configured to automatically inject the built scripts to the html template we provide, which is a simple boilerplate includes `<div id="root"></div>`. `filename: '[name].[contenthash].js'` is to improve compile time by preventing re-compling files if there is no changes since last build.

```js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const config = {
    entry: './client.tsx',
    output: {
        filename: '[name].[contenthash].js',
        path: path.resolve(__dirname, 'dist/client'),
        // clean /dist/client before each build
        clean: true,
    },
    resolve: {
        extensions: ['.ts', '.tsx', '.js', '.json'],
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: 'Create React From Scratch',
            meta: { viewport: 'width=device-width, initial-scale=1.0' },
            template: './index.html',
            inject: 'body',
            publicPath: '/dist/client',
        }),
    ],
    module: {
        rules: [
            {
                test: /\.(ts|js)x?$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                },
            },
        ],
    },
    optimization: {
        // to prevent vendor chunk from being regenerated due to module resolving order is changed
        // when a new dependency is included
        moduleIds: 'deterministic',
        // extract the boilerplate for webpack runtime code
        runtimeChunk: 'single',
        // remove duplicate dependencies from bundles
        splitChunks: {
            chunks: 'all',
        },
    },
};

module.exports = (env, argv) => {
    if (argv.mode === 'development') {
        config.devtool = 'inline-source-map';
    }

    return config;
};
```
babel-loader is a handy plugin for transpiling js code so that the resultant code is compatible across older browser environments. `node_modules` is excluded because we are using packages actively maintained by the community thus we can expect them to be in a good shape for production use. In addition, we have babel-loader configured with a list of presets. Those presets provide pre-defined lists based on the target environment for transpiling js, jsx and ts code.

babel.config.json
```json
{
    "presets": [
        "@babel/preset-env",
        "@babel/preset-react",
        "@babel/preset-typescript"
    ]
}
```

### Server side logic
Now we have our html file ready with necessary scripts injected. The next objective is to inject the html string of the component we want to render into the html file. Here is when `renderToString` comes into play. `renderToString` renders our React component into a html string.
```js
// template.tsx
import { renderToString } from 'react-dom/server';
import { App } from '../src/app';
import React from 'react';

export default function renderHtmlString() {
    return renderToString(<App />);
}
```
In server.ts, parse the html read from the file system and inject the rendered html string into `div#root`.
```js
// server.ts
import fs from 'fs';
import renderHtmlString from './template';
import express from 'express';
import { parse } from 'node-html-parser';
import path from 'path';

// ...
app.get('/', async (_, res) => {
    try {
        const html = fs.readFileSync(
            path.resolve(process.cwd(), 'dist/client/index.html'),
            'utf-8'
        );
        const htmlContent = parse(html);
        const root = htmlContent.querySelector('#root')!;
        root.textContent = renderHtmlString();
        res.send(htmlContent.toString());
    } catch (err) {
        throw err;
    }
});
``` 
In `webpack.config.js`, it is worth mentioning that we use `webpack-node-externals` to exclude packages from node_modules and `externalsPresets: { node: true }` to exclude native packages provided by the environment from being bundled into our code.
```js
const path = require('path');
const nodeExternals = require('webpack-node-externals');

const config = {
    target: 'node',
    entry: path.resolve(__dirname, 'server.ts'),
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, '../dist/server'),
        // clean /dist before each build
        clean: true,
    },
    resolve: {
        extensions: ['.ts', '.tsx', '.js', '.json'],
    },
    plugins: [],
    module: {
        rules: [
            {
                test: /\.(ts|js)x?$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                },
            },
        ],
    },
    // in order to ignore built-in modules like path, fs, etc.
    externalsPresets: { node: true },
    // in order to ignore all modules in node_modules folder
    externals: [nodeExternals()],
    optimization: {
        // to prevent vendor chunk from being regenerated due to module resolving order is changed
        // when a new dependency is included
        moduleIds: 'deterministic',
        // extract the boilerplate for webpack runtime code
        runtimeChunk: 'single',
        // remove duplicate dependencies from bundles
        splitChunks: {
            chunks: 'all',
        },
    },
};

module.exports = (env, argv) => {
    if (argv.mode === 'development') {
        config.devtool = 'inline-source-map';
    }

    return config;
};
```
### Using ECMAScript in Node
You may be wondering how to configure to use ESM imports in Node. Most people suggest to add `"type": "module"` in package.json, which requires to explicitly name the file extension while importing the scripts. We can avoid that by adding a tsconfig.json under `server/` to support ESM imports. You may find this file isn't necessary since babel has support for ts syntax. Babel also doesn't compile options from tsconfig. However, this step is essential if you are using ts-node in your development because ts-node reads from tsconfig file. It wouldn't have recogonised ESM import syntax in otherwise.
```json
{
    "compilerOptions": {
        "target": "esnext",
        "jsx": "react" ,
         /* supports both ECMAScript imports and CommonJS require */
        "module": "NodeNext" ,
    }
}
```
### Babel transpiles your code without type checking, type check your code with typescript
Babel transpiles your code without type checking, which can slip runtime errors into production. In order to prevent that, we should type check our code during build time or even type check in watch mode during development. Here is the tsconfig.json on client side, where `noEmit` is specified for only type checking purpose.
```json
{
    "compilerOptions": {
        "target": "esnext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
        "jsx": "react" /* Specify what JSX code is generated. */,
        "module": "commonjs" /* Specify what module code is generated. */,
        "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
        "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
        "strict": true /* Enable all strict type-checking options. */,

        /* Babel does not type check the code, use tsc for type checking */
        "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
        "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
        "noEmit": true
    }
}
```
tsconfig.json on server side
```json
{
    "compilerOptions": {
        "target": "esnext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
        "jsx": "react" /* Specify what JSX code is generated. */,
        "module": "NodeNext" /* supports both ECMAScript imports and CommonJS require */,
        "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
        "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
        "strict": true /* Enable all strict type-checking options. */,
        "moduleResolution": "NodeNext" /* in sync with "module" config */,

        /* Babel does not type check the code, use tsc for type checking */
        "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
        "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
        "noEmit": true
    }
}
```
Finally, putting everything together in package.json
```json
"scripts": {
        "type-check": "tsc",
        "type-check:watch": "tsc --watch",
        "build:client": "npm run type-check && webpack --mode=production --config webpack.config.js",
        "build:server": "npm run type-check && webpack --mode=production --config server/webpack.config.js --stats-error-details",
        "build": "npm run build:client && npm run build:server",
        "dev:watch": "webpack watch --mode=development --config server/webpack.config.js --config webpack.config.js",
        "prod:watch": "webpack watch --mode=production --config server/webpack.config.js --config webpack.config.js",
        "server": "nodemon dist/server/main.js"
    },
```
Git repo: https://github.com/qianzhong516/create-react-app-from-scratch/tree/isomorphic-react