Saturday, March 24, 2018

React + TypeScript + Hot-Reloading + lazy-loading a module using import and code-splitting

Plus one more thing, NodeJS.

In this post, I'll show you how to make the aforementioned technologies work.

TL;DR. Out-of-the-box, hot-reloading works, but dynamic import won't work. If dynamic import is made to work, both hot-reloading and node won't be able to work. There's a solution to these problems though.


The hot-reloading from react-hot-loader's TypeScript example is not working if code-splitting is used, though the page automatically refreshes whenever the code is changed, the whole page reloads instead of just the part that was changed, and the states reinitializes too. Even if the changes are coming from non-code-splitted code, the whole page reloads and the states gone.

Code-splitting requires setting tsconfig.json's module to esnext, and it also requires removing the babel-loader from webpack.config.js, babel-loader is used by react-hot-loader in order for hot-reloading to work.

If babel-loader is not removed from webpack.config.js's loaders, here's the error of webpack (tsconfig.json's module set esnext, target set to es6) when webpack is splitting the imports:




Here's the error if babel-loader is not removed from webpack.config.js's loaders, with tsconfig.json's module set to esnext, and target set es5:




react-hot-loader works with module set to commonjs only.


Another problem that arises from code-splitting, since it requires setting the tsconfig.json's module to esnext, codes written for node can't be run anymore. At the time of this writing (node version 8), node does not support ES modules. The only way for a TypeScript-generated code be runnable on node is by setting its tsconfig.json's module to commonjs, not esnext.

esnext might be compatible on node version 10: http://2ality.com/2017/09/native-esm-node.html

Here's the error on node when the tsconfig.json's module is set to esnext:



In order to make our TypeScript code be runnable on node, we must set the tsconfig.json's module to commonjs:



Node working: check. Node works by setting the module to commonjs.

Hot-reloading working: check. Hot-reloading is working again as tsconfig.json's module is set back to commonjs.

Lazy-loading a module using import and code-splitting: not yet working. The file theCompany.chunk.js is not splitted from index.js:




We have to find out why when tsconfig.json's module is set to esnext, webpack can split the dynamically-imported module (courtesy of await import) and in turn, emit working webpack code for lazy-loading. Here's the original code:




First, let's look at the code generated from TypeScript when the tsconfig.json's module is set to esnext. Following is the generated javascript code from the TypeScript code above.





The import statement is kept intact. Now let's apply webpack:




With tsconfig.json's module set to esnext, webpack can split the code (theCompany.chunk.js) from the code that uses await import.


When Webpack encounters dynamic import statement, Webpack replaces it with code that can dynamically import a module that works on the browser:





Now let's try setting tsconfig.json's module to commonjs, then run tsc.




Note: for ts.config that has esModuleInterop compilerOptions set to true..
"esModuleInterop": true

..TypeScript generates different code:
return [4 /*yield*/, Promise.resolve().then(function () { return tslib_1.__importStar(require(/* webpackChunkName: 'theCompany' */ '../../../front-end/Company')); })];



Hmm.. that's something different. When the module is set to commonjs, TypeScript generates Promise.resolve instead of keeping the import statement. Now let's check if webpack can apply code-splitting given the code above.



Well that's sad, the file theCompany.chunk.js is not splitted from its parent code. The file theCompany.chunk.js is not generated by webpack. Now let's take a look at the code that dynamically imports the file theCompany.chunk.js.




As the dynamic import statements is not present in the code generated by TypeScript (Promise.resolve+require is generated instead), webpack can't replace them with its own code that can dynamically-load a module from the browser, i.e.,




If we can only nudge TypeScript to keep the import statement despite the tsconfig.json's module is set to commonjs instead of it generating Promise.resolve+require, Webpack will be able to split the code afterwards. Unfortunately, there's no configuration in tsconfig.json that can keep the import statement when tsconfig.json's module is set to commonjs.


With that said, we can use webpack's string-replace-loader to revert all those generated Promise.resolve statements to dynamic import statements instead. Do an npm install or yarn add of string-replace-loader.


To cut to the chase, add the string-replace-loader to webpack.config.js's rules' loaders, then use the regex below to revert back the Promise.resolve to dynamic import statement:


use: [
    {
        loader: 'string-replace-loader',
        options: {
            search: "/\\*yield\\*/, Promise\\.resolve\\(\\)\\.then\\(function \\(\\) \\{[^r]+return require\\(([^)]+)\\);[^)]+\\)",
            replace: "/*yield*/, quickbrown($1)",
            flags: "g"
        }
    },
    'babel-loader',
    'awesome-typescript-loader'
],

Note: for ts.config that has esModuleInterop compilerOptions set to true..
"esModuleInterop": true

..use the following regular expression instead:
search: '/\\*yield\\*/, Promise\\.resolve\\(\\)\\.then\\(function \\(\\) \\{[^r]+return [^(]+\\(require\\(([^)]+)\\)\\);[^)]+\\)'


We revert Promise.resolve+require to quickbrown statement instead of dynamic import statement just to check first if our regex is correct, and check if webpack's string-replace-loader can successfully replace the Promise.resolve+require statement that was generated by TypeScript. Following is the output of the Promise.resolve+require code that was replaced by webpack's string-replace-loader:





The file theCompany.chunk.js is not splitted from index.js since Webpack cannot find any dynamic import statement from the code applied of string-replace-loader, we used quickbrown statement in string-replace-loader.




Now let's change the quickbrown statement to dynamic import statement:

use: [
    {
        loader: 'string-replace-loader',
        options: {
            search: "/\\*yield\\*/, Promise\\.resolve\\(\\)\\.then\\(function \\(\\) \\{[^r]+return require\\(([^)]+)\\);[^)]+\\)",
            replace: "/*yield*/, import($1)",
            flags: "g"
        }
    },
    'babel-loader',
    'awesome-typescript-loader'
],

Note: for ts.config that has esModuleInterop compilerOptions set to true..
"esModuleInterop": true


..use the following regular expression instead:
search: '/\\*yield\\*/, Promise\\.resolve\\(\\)\\.then\\(function \\(\\) \\{[^r]+return [^(]+\\(require\\(([^)]+)\\)\\);[^)]+\\)'


Then run webpack again:




Webpack is now able to split the file theCompany.chunk.js from index.js as it now sees the dynamic import statement.


Here's the generated code by webpack, it's the same as if the tsconfig.json's module is set to esnext:




Hot-reloading working: check.

Lazy-loading a module using import and code-splitting working: check.

Code generated from TypeScript is compatible with node: check.



This post's complete code: https://github.com/MichaelBuen/ssk-two-for-github


Happy Coding!

No comments:

Post a Comment