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
- Webpack configuration
- Adding tsconfig.json
- Adding ESLint config
- Loading dependencies
- Adding React component
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
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')
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 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"]
}
]
}
ngReact
requires loading React and AngularJS dependencies prior to its inclusion as below.
require("angular");
require("react");
require("react-dom");
require("ngreact");
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 */
]);
Let's add the good old hello world component
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.
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
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
<!-- 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.
In order to navigate to AngularJS routes from react components, using window.history.pushState
in React component and that navigates to corresponding angular pages.
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"
}
}