Recently I added code splitting and dynamic imports to a React + TypeScript + WebPack project using React Loadable. However, it took a few hours to work though getting the JS chunks to properly split rather than get bundled together into a single chunk.

React Loadable looks like it requires minimal configuration to work, and TypeScript claims to have out-of-the-box support for dynamic imports. Perhaps with Create React App this is the case, but in existing larger codebases that use WebPack, I had to make some changes, which I’ll highlight below the pasted code.

Stack used here:

  • React 16
  • TypeScript 3
  • WebPack 4

React TSX Class:

import * as React from 'react';
import * as Loadable from 'react-loadable';
import { Icon, message, Spin } from 'antd';
...

interface HasDynamicImportProps {
    propThing: type
}

interface HasDynamicImportState {
  loadableComponent: type
}

export class HasDynamicImport extends React.Component<HasDynamicImportProps, HasDynamicImportState> {

  constructor(props) {
    super(props);
    this.state = {
      loadableComponent: null,
    };
  }
  
  private readonly ANT_ICON = <Icon type='loading' style= spin/>;

  public async componentDidUpdate(previousProps, previousState) {
    /* this component responds to inputs but that is omitted for simplicity */
    this.setAsyncComponent();
  }

  public setAsyncComponent() {
    const loadableInstanceComponent = Loadable.Map({
      loader: {
        AsyncComponentImport: () => {
          return import( /* webpackChunkName: "ComponentWrapper" */ '../components/AsyncComponent');
        },
      },
      loading: () => {
        return null;
      },
      render(loaded, props: any) {
        const Component = loaded.AsyncComponentImport.AsyncComponent;
        const propThing = props.propThing;
        return <Component
          propThing={propThing}
        />;
      },
    });
    this.setState({ loadableComponent: loadableInstanceComponent });
  }

  public render() {
    if (!this.state.loadableComponent) {
      return (
        <div className='flex h-full w-full justify-center'>
          <Spin indicator={this.ANT_ICON} className='mt-8 mb-8'/>
        </div>);
    }
    return (
      <section id='container'>
        <article className='w-screen'>
          <this.state.loadableComponent
            propThing={this.props.propThing}
          />
        </article>
      </section>
    );
  }
}

Highlights from above:

  • Note how this.state.loadableComponent is used to house the loadable component, rather than declaring it as a const outside the class. If declared outside the class as shown in other docs, it exists in memory when you don’t want it to.
  • My example above intentionally does not load a Loading...component but you can do that. The Map is also being used to transfer props. See React Loadable docs.
  • Note how I am importing things, by referencing the exports. TypeScript imports modules a little differently than your typical CRA JS.
  • This handles the passing of internal class values via props, where the props are passed in first through the render. This guards against passing in undefined props on load
  • The inline comment /* webpackChunkName: "ComponentWrapper" */ has a purpose: It tells webpack to code-split this under this chunk name

But what if its compiling fine, but your return import( /* webpackChunkName: "ComponentWrapper" */ '../components/AsyncComponent'); is getting compiled right into the main bundle, and isn’t being split out as it should be?

You should see something like

                                main.0cf7b02de965f7714959.package.js   X KiB                    main  [emitted]  main
              vendor~ComponentWrapper.0cf7b02de965f7714959.package.js  X MiB  vendor~ComponentWrapper  [emitted]  vendor~ComponentWrapper
                         vendor~main.0cf7b02de965f7714959.package.js  X MiB             vendor~main  [emitted]  vendor~main
                                                          index.html  1.81 KiB                          [emitted]  

What if you don’t? What if you’re only seeing main or a single vendor with main? The webpack comment should show up in the list here as a chunk, as it does above.

If you aren’t seeing the above, keep reading…

First, verify your webpack config:

`webpack.web.config.js’

const baseConfig = {/* your other bits */
const plugins = [
  new HtmlWebpackPlugin({
    template: projectRoot + '/src/index.html',
  }),
];
const babelLoader = {
  loader: 'babel-loader',
  options: {
    cacheDirectory: true,
    presets: [['@babel/preset-env']],
    comments: true,
    compact: false,
    plugins: ['@babel/plugin-syntax-dynamic-import']
  },
};
module.exports = merge.smart(baseConfig, {
  entry: {
    main: './src/main.tsx'
  },
  module: {
      rules: [
        {
          test: /\.tsx?$/,
          exclude: /node_modules/,
          use: [babelLoader, { loader: 'ts-loader' }],
        },
  },
  output: {
    path: projectRoot + '/dist',
    filename: '[name].[hash].package.js',
    chunkFilename: "[name].[hash].package.js",
    publicPath: '/',
  },
    optimization: {
        splitChunks: {
            cacheGroups: {
                default: false,
                vendors: false,
                vendor: {
                    chunks: 'all',
                    test: /node_modules/
                }
            }
        }
    },
  plugins,
});

Notes on above:

  • babelLoader has the @babel/plugin-syntax-dynamic-import plugin
  • Some of the optimizations may not be needed in your case, but this webpack config enables the WebPack 4 almost-automatic code splitting to take place. For me, this also splits out fonts automatically.
  • HtmlWebpackPlugin is set to automatically append the relevant JS chunks onto index.html with this config

Second, is your tsconfig correct? It should be using esnext

tsconfig.json

{
  "compileOnSave": false,
  "compilerOptions": {
    "jsx": "react",
    "sourceMap": true,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "target": "es5",
    "module" : "esnext",
    "typeRoots": [
      "node_modules/@types"
    ],
    "lib": [
      "ES2017",
      "DOM",
      "DOM.Iterable"
    ]
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
      "**/*.history"
  ]
}

If this is set to commonjs instead of esnext, dynamic imports will not work and will just get compiled into one of the other chunks. Be warned that esnext is a little experiemental at the time of this writing, and it may interfere with some of your other imports but I was able to work through those just by toggling my import or require syntax as needed in other files.

I want to highlight my terribly-named recent GitHub project “TypeScript-Docker-Node-DI-Starter”, which is meant to combine a stack I’ve become familiar with, but where each component requires some labor to start fresh with.

These are:

  • Docker
  • MySQL in a docker container
  • Node/express, with a persisted connection to be above
  • TypeScript as the languge for the node project, compiling via npm scripts
  • An ORM for MySQL entities
  • OAuth tables, a user table, and fully functional Oauth login flow
  • Dependency Injection
  • A suite of npm scripts and other minor depedenices in the package.json, for convenience

The OAuth implementation is clean, and relies on no existing “All in one” dependencies (just bcrypt and token generators)

I hope this can help someone, just from a standpoint of delivering a boilerplate that no single tutorial online assembles.

View on GitHub

After a few months of evening work and a lot of experimentation, I’ve released an iOS app.

Codable screenshot

Codable provides three primary features:

  • resizable viewport with scaling, allowing you to view any URL as if you were on a smaller or larger iOS screen size
  • JavaScript console support
  • Rendered DOM node navigation and HTML/CSS manipulation

By my knowledge, there are two other functional apps on the iOS app store that do these same functions. It’s a pretty niche use-case to want to do front-end web debugging and editing from mobile I know, but it’s something I’ve wanted to do many times.

Codable leverages one API endpoint quite heavily and uniquely: WKWebView.evaluateJavaScipt()

If you click through on that link, you’ll see that it isn’t too helpful. There is no APIs for grabbing browser errors, the console, the pre-rendered source, or the rendered DOM. So, how do you grab it? Via JavaScript, from the above API. There is pretty much a constant stream of JS dom-query scripts being applied to the browser, and a JSON-ified node list script response is then captured coming out.

Go to Codable on the App Store

If you are struggling to find the cause for Safari not allowing scrolling on a modal, check if you’re using this CSS animation:

animation: drop .5s;

I was struggling for a little while over why a modal that popped in using CSS animations would not scroll in Safari (but would briefly be scrollable before the first .5 seconds I noticed).

Apparently its a Safari behavior (bug?) that causes content to be un-scrollable if the animation property applied to it has run. Removing this property fixed the issue with scrolling in the modal. This does not impact Chrome or Firefox.

When setting up Heroku on a new machine, I ran into this error after running heroku keys:add:

Post https://api.heroku.com/login: x509: certificate signed by unknown authority

Post https://api.heroku.com/login: x509: certificate signed by unknown authority

The solution is to run the following:

HEROKU_SSL_VERIFY=disable heroku login