Patrick Desjardins Blog
Patrick Desjardins picture from a conference

Using TypeScript, React and WebPack

Posted on: 2017-07-24

I created an open source project to bootstrap TypeScript and React few months ago. You can see the first article about TypeScript/React/Gulp before this article. It wasn't bundling the code, and was using Gulp which is in mid-2017 not the preferred tool to package JavaScript code. At this moment, Webpack is the most popular tool allowing to do everything Gulp or Grunt was doing but avoiding to rely on the middle man of having Gulp's package (or Grunt's package) to invoke the actual library. Webpack is also very smart in term of exploring the code and figure out dependencies. This article will focus to migrate from Gulp to Webpack for a TypeScript and React project.

First of all, we need to change index.html. The file was using RequireJs and was not referring to any bundles. The change is dual. We need to remove RequireJs. We will use Webpack to handle to load dependencies between modules. We also need to refer to bundles.

Before:

<html> 
<head> 
  <title>TS + React Boilerplate v1.01</title> 
</head>

<body> 
  <div id="main"></div> 
</body> 
  <script src="vendors/requirejs/require.js"></script> 
  <script src="vendors/jquery/jquery.js"></script> 
  <script> requirejs.config({ 
    //Every script without folder specified before name will be looked in output folder 
    baseUrl: 'output/', 
    paths: { 
      //Every script paths that start with "vendors/" will get loaded from the folder in string 
      vendors: 'vendors', 
      jquery: '../vendors/jquery/jquery', 
      react: "../vendors/react/dist/react", "react-dom": "../vendors/react-dom/dist/react-dom" }
    }); 
    //Startup file requirejs(['file1']); 
  </script>
</html> 

After:

  <html> 
  <head> <title>TS + React Boilerplate v1.01</title> </head> 
  <body> 
    <div id="main"></div> 
  </body> 
  <script src="vendorbundle.js"></script> <script src="appbundle.js"></script> 
</html> 

RequireJs' configuration and the startup file are gone. The complexity will move into Webpack's configuration file that we will see soon. So, at this point, you see that we won't be using AMD. This mean that we need to build our TypeScript to use something else. We will use CommonJS.

{ 
   "compilerOptions": { 
     "sourceMap": true, 
     "target": "es6", 
     "module": "commonjs", 
     "outDir": "./deploy/output", 
     "jsx": "react", 
     "noImplicitAny": true 
    }, 
    "exclude": [ "node_modules", "**/*.spec.ts" ] 
} 

since Webpack will read the EcmaScript syntax used in each file of each module, it will transpile in CommonJS format. The JavaScript produced is read by Webpack and this one will bring all the file into a single one (bundle). This remove the need to load asynchronously (like AMD) was doing.

Changing to CommonJS made the code to require a change. If you want to load a relative to the file that want to import a module, it needs to start with ./ instead of directly the name. For example, you won't be able to write :

import { Component } from "component1";"

but

 import { Component } from "./component1";

The next change was around JQuery. The file that was using JQuery didn't had any reference to JQuery, but now we explicitly mention the library.

import * as $ from "jquery"; 

Before going in Webpack configuration, we had with AMD a lazy loading file that was loading and using a specific file after 2 seconds. This is to simulate the "load on-demand" files that we may want not to load initially. The scenarios are multiple. This can be justify because the user is rarely using the feature, hence no need to load this one. This can also be that the user doesn't have the authorization to do this kind of action, thus no need to load code that won't be used.

Here is the AMD solution we had before:

 import foo = require("folder1/fileToLazyLoad"); 
 export class ClassB { 
   public method1(): void { 
     console.log("ClassB->method1"); 
     setTimeout(() => { 
       requirejs(["folder1/fileToLazyLoad"], (c: typeof foo) => { const co = new c.ClassC(); 
       co.method1(); }); 
      }, 2000); 
    } 
  } 

The first line is to tell TypeScript the type we want to lazy load. It was using "require" which we will still use. This time, we can use the relative path. So far, not much as change. However, we can see that we were using requirejs directly inside the timer. This time, we will use CommonJS and load the module. It's almost the same thing -- using a different library.

import foo = require("./fileToLazyLoad"); 
export class ClassB { public method1(): void { 
  console.log("ClassB->method1"); 
  setTimeout(() => { 
    System.import("./fileToLazyLoad")
      .then((c: typeof foo) => { 
        const co = new c.ClassC(); 
        co.method1(); 
      }); 
    }, 2000); 
  } 
} 

We are at the point where bigger change will occurs. The change start with NPM module that we need to use. As we saw, we can remove RequireJS from the dependencies list. We also need to bring many libraries for Webpack, loader and utility library to clean and move files. Here is the complete list of dependencies:

"devDependencies": { 
  "@types/express": "^4.0.35", 
  "@types/jquery": "^2.0.46", 
  "@types/react": "^15.0.26", 
  "@types/react-dom": "^15.5.0", 
  "@types/systemjs": "^0.20.2", 
  "awesome-typescript-loader": "^3.1.3", 
  "copyfiles": "^1.2.0", 
  "del-cli": "^1.0.0", 
  "express": "^4.15.3", 
  "file-loader": "^0.11.2", 
  "html-webpack-plugin": "^2.28.0", 
  "source-map-loader": "^0.2.1", 
  "typescript": "^2.3.4", 
  "webpack": "^2.6.1", 
  "tslint": "^5.4.2" 
}, 
"dependencies": { 
  "jquery": "^3.2.1", 
  "react": "^15.5.4", 
  "react-dom": "^15.5.4" 
} 

The next step is that we won't use Gulp to invoke action. If we want to delete previous generated deploy files, build TypeScript or run the server, we need to use the CLI of each of the tool we are using. We could use directly the TypeScript's CLI, named tcs, and use xcopy to move file, etc. The problem is that it is not easy to remember. NPM allows to have custom script which can be invoked with npm use ABC where "ABC" is the name of your script. Here is the script we need to add to replace the Gulp tasks we had.

"scripts": { 
  "clean": "del-cli deploy/**", 
  "package": "./node_modules/.bin/webpack --config webpack.config.js --display-error-details", 
  "copy": "copyfiles -u 1 ./app/index.html ./deploy", 
  "build": "del-cli deploy/**  SET NODE_ENV=development  webpack --config webpack.config.js --display-error-details  copyfiles -u 1 ./app/index.html ./deploy/", 
  "server": "node bin/www.js", 
}, 

The "clean" script use the "del-cli" to delete the deployment folder. This could be use a native Windows or Linux command, but using this tool allows to be cross platform. The "package" allows to run webpack with a specific configuration file and for debugging purpose to display verbose detail. The principle is the same for all others script. The "build" one is using the clean + package + copy. So, in practice, you should use only build and server.

The final step is to configure Webpack. This is done in the file webpack.config.js. You can rename it the way you want, you just need to specify it in the webpack command after --config. Webpack can use many NPM package to accomplish its job. For sure, you need at least the "webpack" package.

var path = require('path'); var webpack = require('webpack'); 

Webpack needs to have an entry point. In the example we are working on, the main file is the one that was used in the index.html with requirejs(['file1']);. This time, we do not have any indication in the HTML file. However, Webpack needs one, or many, entry point and will navigate through all the dependencies to make the main bundle.

module.exports = { entry: { app: "./app/scripts/file1.tsx" }, 

Entry may be the entry point, Output will be where the bundles goes. The filename uses the square bracket which will be replaced by the key provided by each entry point. In our example, we have "app" in the entry, and we will have a file produced with the name "appbundle.js" in the output. The directory is provided at the "path" property, which in that case is "deploy".

output: { path: path.resolve(__dirname, 'deploy'), filename: '[name]bundle.js' }, 

The next configuration is to tell which extension Webpack should care of. For us, it's TypeScript

resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'] }, 

This property allows to have source map. This give the possibility to debug TypeScript in Chrome on the real individual file even if it's bundled.

devtool: "source-map", 

Webpack work with rules. Every "test" evaluate a condition to execute a loader. We use two different ones. One to call the TypeScript loader that will transpile TS and TSX file. The second one is to generate source map. Even if we have the devtool to provide source map, we need a central place to handle third-party JS library too.

module: { 
  rules: [ 
    { test: /\\.tsx?$/, loader: "awesome-typescript-loader" },
    { enforce: "pre", test: /\\.js$/, loader: "source-map-loader" }, 
  ] 
}, 

The last piece is a plugin called CommonsChunkPlugin. It comes from Webpack and its role is to great additional bundle. In that case, we create a bundle name "vendorbundle.js". The minChunk can be a number of time we see the reference to join the bundle. For example, if we have a module that we use often and that we want them to be in a common bundle, we can say "5" and if more than 5 modules reference than instead of being in the "app" one, it would go in this one. For us, we want to have all vendors module, which mean they are from node_modules directory. To do so, the minChunks allows to pass a function. When it contains the node_modules in the path, it goes into that bundle instead of the main one (app).

plugins: [ 
  new webpack.optimize.CommonsChunkPlugin(
    { name: "vendor", 
    filename: "vendorbundle.js", 
    minChunks: function(module) { 
      return module.context  module.context.indexOf('node_modules') !== -1; } 
    }) 
  ] }; 

You can find the exact code from this commit in GitHub. In this article, we saw how to remove Gulp to use Webpack. Not only it removes dependencies to Gulp and Gulp's packages, it also bring a powerful tool to bundle smartly. The next step will be to bring an auto-reload when the code change to have TypeScript compile automatically to get JS deployed.