Skip to content

Instantly share code, notes, and snippets.

@aishwarya257
Created April 6, 2021 14:41
Show Gist options
  • Save aishwarya257/189d15caf00c2fd11e70437135f9ba72 to your computer and use it in GitHub Desktop.
Save aishwarya257/189d15caf00c2fd11e70437135f9ba72 to your computer and use it in GitHub Desktop.

Migrating Angular JS app to React

We often have to deal with legacy code at some point. In our organization, we have an app that uses AngularJS framework which was not touched for quite some years. Recently, we were tasked to add a new feature to that app. The app had a quite a lot of outdated dependencies with dependencies coming from both bower.json and package.json with the build tool using older versions of Gulp and Webpack.

We tried to add the new feature without upgrading any of the packages but it resulted in a lot of pain. Even a small change to old AngularJS framework was tiresome.

Upgrading to the latest version of Angular required a complete rewrite of the entire app. Rewriting an entire application needs a lot of man work. So, we decided to look for options that offer incremental upgrade.

All the apps in our organization use React, hence we decided to move forward to introduce React to this project and I took the responsibility to do it. After some research on Google, some libraries like ngReact, react2angular, angular2react offer migration to React from legacy Angular JS code.

Since the app uses v1.2.28 of AngularJS, ngReact is the only package that offered support as other packages are targeted for higher versions.

Now, let's see how to introduce React + Typescript into an Angular JS app using ngReact along with tools like Prettier, ESLint etc.,

This involves the changes in following areas

Install dependencies

Install the necessary dependencies. The demonstration uses yarn for dependency inclusion.

Add the following as devDependencies

yarn add typescript @types/react @types/react-dom @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier eslint-plugin-react eslint-plugin-react-hooks prettier -D

Add the following as dependencies

yarn add react react-dom ngreact

Webpack configuration

Add the necessary dependencies for webpack

yarn add ts-loader fork-ts-checker-webpack-plugin -D

The project used a combination of Gulp and Webpack for bundling. As a first step, I upgraded all the dependencies pertaining to Gulp and Webpack. Since removing Gulp needed a lot of effort, we decided to remove it at a later stage. Adjust the webpack configuration as follows:

const forkTsCheckPlugin = new ForkTsCheckerWebpackPlugin({
  eslint: {
    files: process.cwd() + "/src/**/*.tsx",
  },
});

const webpackConfig = {
  entry: {
    app: ["./src/vendors.js", "./src/app.js"],
  },
  mode: "development",
  output: {
    /** Output configurations */
  },
  devtool: false,
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: require.resolve("babel-loader"),
            options: {
              presets: [
                [
                  require.resolve("@babel/preset-env"),
                  {
                    modules: false,
                    useBuiltIns: "entry",
                    corejs: 2,
                  },
                ],
              ],
            },
          },
        ],
        exclude: /node_modules/,
      },
      {
        test: /\.(tsx?)$/,
        loader: require.resolve("ts-loader"),
        options: {
          transpileOnly: true,
          logLevel: "warn",
          experimentalWatchApi: true,
        },
        exclude: /node_modules/,
      },
      {
        test: process.cwd() + "/node_modules/angular/angular.js"),
        loader: "exports?window.angular",
      },
    ],
  },
  resolve: {
    /** Other parts of code */
    extensions: [".js", ".tsx", ".ts"],
  },
  plugins: [forkTsCheckPlugin],
  resolveLoader: {
    modules: [path.join(process.cwd(), "node_modules")],
  },
};

As you can see in the above example, ts-loader manages the files with extension .ts and .tsx and it is advised to include transpileOnly option to true, so that fork-ts-checker-webpack-plugin takes care of typechecking.

If you use AngularJS < 1.3.14, it is also important to add the corresponding loader as seen in the snippet to recognize require('angular')

Include tsconfig.json

In order to enable Typescript, add the tsconfig.json with allowJs option set to true and jsx option set to react-jsx. Setting this option, allows you to skip React import in every file for newer versions of React.

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "strict": true,
    "allowJs": true,
    "noEmit": false,
    "jsx": "react-jsx",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/"]
}

Add ESLint config

Add the necessary plugins of your choice like below. In order to avoid typescript linting on js files, apply @typescript-eslint plugin only for files with extension .ts and .tsx in overrides section as seen in the snippet

{
  "extends": [
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
    "plugin:prettier/recommended",
    "plugin:cypress/recommended"
  ],
  "env": {
    "es6": true,
    "node": true,
    "browser": true,
    "jest": true
  },
  "parser": "@typescript-eslint/parser",
  "plugins": ["react-hooks", "import", "jest"],
  "parserOptions": {
    "ecmaVersion": 2018,
    "sourceType": "module",
    "ecmaFeatures": {
      "modules": true,
      "jsx": true
    }
  },
  "ignorePatterns": ["/node_modules/**", "/dist/**", "/bower_components/**"],
  "rules": {
    "no-multiple-empty-lines": "error",
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn",
    "react/jsx-uses-react": "off",
    "react/react-in-jsx-scope": "off",
    "react/prop-types": "off",
  },
  "settings": {
    "react": {
      "version": "detect"
    }
  },
  "globals": {
    "angular": true,
    "_": true,
    "jasmine": true,
    "inject": true
  },
  "overrides": [
    {
      "files": ["*.ts", "*.tsx"],
      "extends": ["plugin:@typescript-eslint/recommended"]
    }
  ]
}

Loading dependencies

ngReact requires loading React and AngularJS dependencies prior to its inclusion as below.

vendors.js

require("angular");
require("react");
require("react-dom");
require("ngreact");

Including react directive

In your root module, include react as the first module before all others as follows:

angular.module("my-app", [
  "react",
  "ui.router",
  "ngAnimate",
  /** ... */
  /** All other angular modules */
]);

Adding a component

Let's add the good old hello world component

Utility function

In order to convert a React component into a AngularJS directive, create a function as follows:

export const createAngularDirective = (Component, props) => {
  const AngularDirective = (reactDirective) => reactDirective(Component, props);
  AngularDirective.$inject = ["reactDirective"];
  return AngularDirective;
};

These three lines of code does the conversion. This function takes the React component and the array of property strings as arguments and convert it to a angular directive.

React component

HelloWorld.tsx

function HelloWorld({ name }) {
  return <h1> Hello, {name}</h1>;
}

export default createAngularDirective(HelloWorld, ["name"]);

Pass the component and the props as an array like ["name"] and export the return value from the createAngularDirective as provided in the snippet above

Creating Angular Directive

sample.module.js

import HelloWorld from "./HelloWorld";
import controller from "./sample-controller";

export default angular
  .module("my-app.sample", [controller])
  .directive("helloWorld", HelloWorld)
    /** Rest of the code */
  }).name;

Now, one could access <hello-world></hello-world> directive within AngularJS HTML

Using the directive within HTML

sample.html

<!-- Rest of AngularJS HTML -->

<hello-world name="name"></hello-world>

<!-- Rest of AngularJS HTML -->

Here, the value of "name" comes from AngularJS scope, ie: $scope.name. This way, one can use React component within AngularJS module and pass props as well.

Navigation between routes

In order to navigate to AngularJS routes from react components, using window.history.pushState in React component and that navigates to corresponding angular pages.

Configure husky

Husky hooks allows prettifying files. The following snippet shows an example of prettifying the files using the pre-commit hook

yarn add husky pretty-quick -D

Add the following snippet to your package.json or add it separately in .huskyrc file

"husky": {
    "hooks": {
        "pre-commit": "pretty-quick --staged"
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment