Code splitting React components with TypeScript and NO Babel
Tags: react, typescript, webperf
Published: 2020-04-02 14:10:19

---
title: Code splitting React components with TypeScript and NO Babel
published: true
date: 2020-03-12 17:20:38 UTC
tags: react,typescript,webperf
canonical_url: https://blog.logrocket.com/code-splitting-react-components-with-typescript-and-no-babel/
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/u8qgp1gncwv6ox4f8h57.png
---
**Written by [Paul Cowan](https://blog.logrocket.com/author/paulcowan/)**✏️

## The secret to web performance is less code

With the advent of the now-infamous single page application, extreme amounts of JavaScript started getting pushed to the browser. The sheer weight of JavaScript is one problem, but the browser also has to parse the downloaded JavaScript. The UI thread of the browser can hang under such duress as it is pushed beyond its intended purpose. The obvious answer is to ship less code. Code splitting allows us to do that without shipping fewer features.

Code splitting is a complicated business where a bundle of code is split into smaller chunks that can be loaded on demand. Thankfully tools like webpack abstract this complexity behind a less complicated API. Unfortunately, this less complicated API is still very complex. In the React ecosystem, tools like [loadable-componets](https://github.com/gregberge/loadable-components) add a much simpler veneer of sanity around [dynamic imports](https://javascript.info/modules-dynamic-imports).

[![LogRocket Free Trial Banner](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/f760c-1gpjapknnuyhu8esa3z0jga.png?resize=1200%2C280&ssl=1)](https://logrocket.com/signup/)

## Code splitting by route

I want to see more rendering control returned to the server. The browser is not meant to render HTML, and there are many good reasons why rendering React server-side is preferable. I am predicting that we will see a return to more HTML rendered server-side.

Below is some code from [my company](https://cutting.scot) website that uses dynamic imports to create smaller code files that get loaded on demand.

```jsx
import React from 'react';
import loadable from '@loadable/component';
import * as Urls from '../urls';
import { RouteProps, Route, Switch } from 'react-router';

export type Page<P = unknown> = RouteProps & {
  heading: string;
  path: string;
  footerPage?: boolean;
} & P;

const fallback = <div>loading....</div>;

const Home = loadable(() => import('src/components/Home'), {
  fallback,
});
const OSS = loadable(() => import('src/components/OSS'), {
  fallback: <div>Loading...</div>,
});
const Blog = loadable(() => import('src/components/Blog'), {
  fallback: <div>Loading...</div>,
});

export const routable: Page[] = [
  {
    heading: 'Home',
    path: Urls.Home,
    component: Home,
    exact: true,
  },
  {
    heading: 'OSS',
    path: Urls.OSS,
    component: OSS,
    exact: true,
  },
// etc.
```

The `loadable` function takes a dynamic import as an argument and will do the hard work for us. Running a webpack build creates several smaller files that can be lazy-loaded:

![list of files that have been code splitted](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2020/03/code-split.png?resize=828%2C439&ssl=1)

## @loadable/babel-plugin

I am a big TypeScript fan, and I have always stayed away from anything requiring Babel as having to maintain two different transpiler configurations is not a road I am willing to travel.

The `@loadable/babel-plugin` transforms code like this:

```jsx
import loadable from '@loadable/component';

export const LazyFoo = loadable(() => import('./input/AsyncDefaultComponent'));
```

into code like this:

```jsx
import loadable from 'loadable-components';

export const LazyFoo = loadable({
  chunkName() {
    return 'input-AsyncDefaultComponent';
  },
  isReady(props) {
    return (
      typeof __webpack_modules__ !== 'undefined' &&
      Boolean(__webpack_modules__[this.resolve(props)])
    );
  },
  requireAsync: () =>
    import(
      /* "webpackChunkName":"input-AsyncDefaultComponent" */ './input/AsyncDefaultComponent'
    ),
  requireSync(props) {
    return typeof '__webpack_require__' !== 'undefined'
      ? __webpack_require__(this.resolve(props))
      : eval('module.require')(this.resolve(props));
  },
  resolve() {
    if (require.resolveWeak)
      return require.resolveWeak(
        /* "webpackChunkName":"input-AsyncDefaultComponent" */ './input/AsyncDefaultComponent',
      );
    else
      return eval('require.resolve')(
        /* "webpackChunkName":"input-AsyncDefaultComponent" */ './input/AsyncDefaultComponent',
      );
  },
});
```

## loadable-ts-transformer

Now enters the hero of the piece, namely the [loadable-ts-transformer](https://github.com/Quramy/loadable-ts-transformer) which does the same job as its Babel counterpart only it does this by creating a [TypeScript transformer](https://blog.logrocket.com/using-typescript-transforms-to-enrich-runtime-code-3fd2863221ed/). A TypeScript transformer allows us to hook into the compilation pipeline and transform code just like is listed above with the Babel plugin. A full AST is at the developer’s disposal to bend to their will.

## Hooking up the loadable-ts-transformer to a webpack build

The first step is to define the components that we want to split into smaller chunks with the [loadable-component’s loadable function](https://loadable-components.com/docs/api-loadable-component/#loadable):

```jsx
const Home = loadable(() => import('src/components/Home'), {
  fallback,
});
```

Next, webpack needs configured. Typically in a webpack ssr (server-side rendered) build, you have a server webpack configuration file and a client webpack configuration file.

The webpack server configuration takes care of bundling the node [express code](https://expressjs.com/) that renders the react components server-side.

To keep duplication down between the two configuration files, I use [webpack-merge](https://www.npmjs.com/package/webpack-merge) to create a `common.config.js` file that is merged into both the `client.config.js` and `server.config.js` files.

Below is an example of a `common.config.js` file that has the common components for both the webpack client and server configuration files:

```jsx
const path = require("path");
const { loadableTransformer } = require('loadable-ts-transformer');

module.exports = {
  resolve: {
    extensions: ['.ts', '.tsx', '.js'],
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        loader: 'ts-loader',
        options: {
          transpileOnly: true,
          getCustomTransformers: () => ({ before: [loadableTransformer] }),
        },
      }
    ],
  },
};
```

I use [ts-loader](https://github.com/TypeStrong/ts-loader) to transpile TypeScript into JavaScript and `ts-loader` has a [getCustomTransformers](https://github.com/TypeStrong/ts-loader#getcustomtransformers) option which we can use to add the `loadable-ts-transformer`.

The `client.config.js` file looks like this:

```jsx
const path = require("path");
const merge = require('webpack-merge');
const LoadablePlugin = require('@loadable/webpack-plugin');
const commonConfig = require('./webpack.config');
const webpack = require('webpack');

module.exports = () => {
  return merge(commonConfig, {
    output: {
      path: path.resolve(__dirname, 'public'),
      publicPath: '/assets/',
      filename: '[name].[chunkhash].js',
    },
    entry: {
      main: path.resolve(__dirname, 'src/client.tsx'),
    },
    optimization: {
      splitChunks: {
        name: 'vendor',
        chunks: 'initial',
      },
    },
    plugins: [
      new LoadablePlugin(),
      new webpack.DefinePlugin({ __isBrowser__: "true" })
    ],
  });
};
```

Note the use of the [webpack.DefinePlugin](https://webpack.js.org/plugins/define-plugin/) to add an ` __isBrowser__ ` property into the bundled code. This stops having to use endless `typeof window === 'undefined'` checks to determine if code is executing on the server or browser.

The `client.config.js` file also adds the [@loadable/webpack-plugin](https://loadable-components.com/docs/api-loadable-webpack-plugin/#loadablewebpack-plugin) to the plugin array. Do not add this to the `server.config.js`.

The `server.config.js` file looks like this:

```jsx
const path = require("path");
const merge = require('webpack-merge');
const commonConfig = require('./webpack.config');
const webpack = require('webpack');
const nodeExternals = require('webpack-node-externals');

module.exports = () => {
  return merge(commonConfig, {
    target: 'node',
    externals:  nodeExternals({
      whitelist: [
          /^@loadable\/component$/,
          /^react$/,
          /^react-dom$/,
          /^loadable-ts-transformer$/,
        ]
      }),
    ],
    output: {
      path: path.resolve(__dirname, 'dist-server'),
      filename: '[name].js',
    },
    entry: {
      server: path.resolve(__dirname, 'src/server.tsx'),
    },
   plugins: [
     new webpack.DefinePlugin({ __isBrowser__: "false" })
   ]
  });
};
```

The [webpack externals](https://webpack.js.org/configuration/externals/) section has tripped me up many times. The externals property allows you to whitelist what gets bundled in a webpack server build. You do not want to be bundling the entirety of the `node_modules` folder. I find the [webpack-node-externals package](https://www.npmjs.com/package/webpack-node-externals) which has a whitelist option extremely useful.

## loadable-components server-side

The `server.config.js` file defines and [entry point](https://webpack.js.org/concepts/#entry) of `src/server/index.ts` which looks like this:

```jsx
export const app = express();
const rootDir = process.cwd();

const publicDir = path.join(rootDir, isProduction ? 'dist/public' : 'public');
app.use(express.static(publicDir));

app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

app.get('/*', async (req, res) => {
  await render({
    req,
    res,
  });
});
```

The important points of the above code are:

- The `app.use(express.static(publicDir));` code points express to the static files that are outputted by webpack using the [express static function](https://expressjs.com/en/starter/static-files.html)
- A catchall `app.get('/*. async (req. res) => {` route points to a reusable `render` function that I will explain next

The `render` function is listed below:

```jsx
const statsFile = path.resolve(process.cwd(), 'dist/loadable-stats.json');

export async function render({ req, res }: RendererOptions): Promise<void> {
  const extractor = new ChunkExtractor({
    entrypoints: ['client'],
    statsFile,
  });

  const context: StaticRouterContext = {};

  const html = renderToString(
    extractor.collectChunks(
      <StaticRouter location={req.url} context={context}>
        <Routes />
      </StaticRouter>,
    ),
  );

  res.status(HttpStatusCode.Ok).send(`
    <!doctype html>
    <html lang="en">
      <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta httpEquiv="X-UA-Compatible" content="IE=edge" />
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        ${extractor.getStyleTags()}   
      </head>
      <body>
        <div id="root">${html}</div>
        ${extractor.getScriptTags()}
      </body>
    </html>
`);
}
```

The code above makes use of the [ChunkExtractor component](https://loadable-components.com/docs/api-loadable-server/#chunkextractor) that collects chunks server-side and then creates script tags or script elements that can be used in the outputted HTML.

`${extractor.getStyleTags()}` will output the CSS link tags and `${extractor.getScriptTags()}` will output the JavaScript script tags.

When running your build, the @loadable/webpack-plugin generates a file called `loadable-stats.json`, which contains information about all the entries and chunks from webpack.

Once that’s in place, `ChunkExtractor` is responsible for finding your entries from this file.

The [entryPoints array](https://loadable-components.com/docs/server-side-rendering/#chunkextractor-entrypoints) of the `ChunkExtractor` component is set to `['client']` which maps to the `client` property of the webpack `client.config.js` file:

```jsx
entry: {
  client: path.join(process.cwd(), 'src/client.tsx'),
 },
```

## Client rehydration

The client config file’s [entry point](https://webpack.js.org/concepts/#entry) is now an object with a client property:

```jsx
entry: {
  client: path.join(process.cwd(), 'src/client.tsx'),
 },
```

The `client.tsx` file is listed below:

```jsx
import React from 'react';
import { hydrate } from 'react-dom';
import { loadableReady } from '@loadable/component';

import { App } from '../containers/App';

const bootstrap = (): void => {
  const root = document.getElementById('root');

  if (!root) {
    return;
  }

  hydrate(<App />, root);
};

loadableReady(() => bootstrap());
```

Normally when rehydrating React server-side rendered code, you would use [ReactDom’s hydrate function](https://reactjs.org/docs/react-dom.html#hydrate) but in the `loadable-component's` world above, the loadable-component’s [loadableReady function](https://loadable-components.com/docs/api-loadable-component/#loadableready) is used to wait for all the scripts to load asynchronously to ensure optimal performances. All scripts are loaded in parallel, so you have to wait for them to be ready using `loadableReady`.

## Epilogue

I have avoided using many of the code splitting packages because of the need for Babel. The[loadable-ts-transformer](https://github.com/Quramy/loadable-ts-transformer) has cured this.

If you would like to see this added to the [loadable-component’s source](https://github.com/gregberge/loadable-components) then please chime in on [this issue](https://github.com/gregberge/loadable-components/issues/458) where I discovered about its existence.

* * * 
## Full visibility into production React apps 

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, [try LogRocket.](https://www2.logrocket.com/react-performance-monitoring)

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/eq752g8qhbffxt3hp9t4.png)

[LogRocket](https://www2.logrocket.com/react-performance-monitoring) is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — [start monitoring for free.](https://www2.logrocket.com/react-performance-monitoring)
* * *

The post [Code splitting React components with TypeScript and NO Babel](https://blog.logrocket.com/code-splitting-react-components-with-typescript-and-no-babel/) appeared first on [LogRocket Blog](https://blog.logrocket.com).