Upgrading Angular
Upgrading Angular - Lessons from a 13 to 18 move.
Photo by Tudor Baciu on Unsplash
Upgrading Angular - Lessons from a 13 to 18 move
This guide highlights some steps for consideration when upgrading the version of Angular you are using. This is based mainly on my experience of upgrading a project from Angular 13 to Angular 18. All the steps and options listed below have been applied for that upgrade. However, proceed with caution, all those changes required extensive research to understand the implications of the change fully and this was backed by cumbersome developer testing and a plan for a full test regression cycle before a production release.
The time will come to upgrade your Angular version. It is best to not lag too far behind as it becomes a bigger chore to eventually catch up, especially if you are now forced to do it due to the need for new features or packages that are only available for a later version of Angular.
⚠️ There are multiple steps to go through for these upgrades. Make sure to do a step at a time, build and test, and address any issues, before moving on to the next step. Doing a big bang is a shortcut way to failing the whole upgrade and eventually ditching it.
Do not do the upgrades by hand, but instead make use of the official update tools, using the tool available here. The tool allows you to choose the from and to version and provides a guide. If you are upgrading across multiple versions, I would recommend doing it one version at a time fully testing things and addressing any issues before progressing to the next version. Make sure to go through and tick other dependencies like Angular Material and whether or not you are on Windows.
It is best to select the Advanced application complexity in the tool so it gives you detailed guides on what to look out for and you can just ignore what is not relevant in your case. If not sure, do a code search for the mentioned components or APIs that should be updated.
The process boils down to running the command ng update @angular/core@xx @angular/cli@xx
for the target version and going through the rest of the guide for any manual changes that are required. If you are working with material you will also be asked to run the command ng update @angular/material@xx
. The tool will make any required changes to your package.json
file and install the packages. This includes core angular dependencies, angular tooling and dependencies like TypeScript.
Make sure you have no pending changes in GIT before running the commands and commit after each step.
Check the node compatibility for the version of Angular you are targeting as you may also need to upgrade that. I highly recommend you make use of nvm, so you can work with multiple versions of node on your machine, especially if you work across multiple projects.
Breaking changes
You may encounter breaking changes, depending on your project. Here are some examples of things I ran into while upgrading from Angular 13 to 18.
- Angular 15 requires form models to now have a generic type parameter. You can still opt out by using the untyped version of the form modal classes.
- Some dependencies,
rxjs
being one you will likely run into and particularly component libraries, will have versions that support specific Angular versions so as you update Angular you will have to bump some of these up too. - The
package-lock.json
file can get corrupted and track incorrect information. If yournpm install
starts to fail oddly or complain about packages missing that are in thepackage.json
file, it may be best to manually delete thepackage-lock.json
file and runnpm install
again. If this does not work, delete thenode_modules
folder, runnpm cache clean --force
thennpm install
. - Angular 16 drops the
entryComponents
option in modules. - Angular 18 does not accept certain special characters in string literals in HTML templates, like the
@
character which I had to replace with@
. The failing build was very clear about this issue and made a change recommendation. - When updating material there is a chance that some of the look and feel in your application may change and some weird UI bugs may also pop up, for example, controls no longer being positioned properly. Make sure to fully test things.
These are just examples of some breaking changes you can encounter. The good news is that any Angular-related changes I ran into were already mentioned in the upgrade tool with recommendations.
I am quite pleasantly surprised at how useful the Angular upgrade tool is and how much it automates, combined with how well the build gives detailed errors for anything that needs attention. While these upgrades remain quite tedious and very demanding in terms of testing, the tooling did make this significantly better.
New Angular, now what?
Now you have the new Angular and all the new features at your fingertips and any of the benefits like improved build and run times etc. One beast you may want to confront already is that over time you may start to have inconsistencies in your project. For instance if you now suddenly have access to the new standalone components, do you now only use them going forward and retain the modules you already have?
That is a tough question and the answer may largely depend on how complex your project is and how bold you are feeling. I would however recommend migrating as much as you can to use new features if you are already going to invest in tons of regression testing.
Migrate to use standalone components
You can migrate to use the standalone components using the official tooling ng generate @angular/core:standalone
, see more documentation here. This will migrate in 3 steps
- Convert all components, directives and pipes to standalone
- Remove redundant NgModule classes
- Bootstrap the application using standalone APIs
I highly recommend biting the bullet and doing this one as this highly reduces boilerplate code.
When I did this I had to address a few things manually. Suddenly some components that were using
app-sidebar
that does not exist with Angular 18 only started to fail the build after this move. A manual step we have to pull app configuration was also not migrated well and only done for the app root, but not for child components that required it.
Migrate the material theme
If you are working with material and have updated across Angular material versions that also have an update to the material specification, for example from the material specification 2 to 3, you may need to update your base theme if you are using any, as those target a specific specification, documentation here. In our scenario we had the deeppurple-amber.css as the base and we chose to move over to azure-blue.css.
Migrate control flow syntax
You can migrate to the new and shiny control flow syntax available since Angular 17 that simplifies the use of directives like *ngFor
, *ngIf
and *ngSwitch
. This can be run using the official tooling ng generate @angular/core:control-flow
Other migrations
There are other migrations that you may be interested in that have tooling support or are fully documented. See the available migrations here.
angular.json
Depending on the version update you may also want to make changes to your angular.json
config file.
browserTarget
is dropped forbuildTarget
defaultProject
is now obsoletedefaultCollection
is now obsolete
You can find out what needs to be changed by opening the file in your IDE to see any warnings. Comparing what you have with the default that is generated for a new project on the same Angular version is also a good idea.
tsconfig.json
Given that the version of TypeScript would have been updated, you should also look to see what configuration can be updated in the tsconfig.json
file
- Consider newer settings like
- noPropertyAccessFromIndexSignature
- noImplicitOverride
- esModuleInterop
- skipLibCheck
- moduleResolution move from
node
tobundler
- bumping up the ECMAScript version under
target
,module
andlib
Other dependencies
This is a good time to look into upgrading all other dependencies, and in particular, some dependencies will be tied to a particular version of Angular, Node or TypeScript, which would also likely have changed.
Make sure to read the official documentation of each package to see what has changed and check the compatibility information. If you are far behind you are very likely to meet many packages with API changes.
⚠️ Update packages one at a time making sure to build and test your project afterwards. A big bang update and then having issues is a nice way to get bogged fail here.
Consider making use of the command npm outdated
to track packages that need updates through a convenient report.
Also, consider making use of the VS Code extension, Version Lens for convenient information directly in your package.json
file with upgrade actions directly from there.
Specific dependencies you may want to immediately update if you depend on are
- rxjs
- component libraries, i.e. ngx-bootstrap
- eslint
Hopefully you can update all packages and get to see this nice report on npm install
.
Cleanup
We took the Angular upgrade as an opportunity to identify and clean up any unused packages. That is a little less to worry about upgrading and this can also help keep bundle sizes reasonable.
Unused packages are not as simple as checking the package.json
and finding something that is not used at all. Some may be used in modules/components that are not used in your project which would mean also deleting those artefacts.
Package.json
Make sure the common CLI tasks that developers run are available as scripts in the package.json
file. Examples are serving, building, testing and linting.
Add an engines section so that the team knows and has a reference point for the supported runtimes like node. Example
"engines": {
"node": "20.15.1",
"npm": "10.7.0"
}
⚠️ Make sure no dependencies that should be for development only are in the main dependencies list.
Package-lock.json
When making multiple changes and in particular when the version of node has been updated, the package-lock.json
can at times get corrupted and have incorrect information. It is a good idea to delete the file once done with the upgrade and run npm install
again to have the file re-populated.
If you manage to update all dependencies, great. If not, it is still worth running an audit fix with
npm run audit --fix
to attempt to address packages with known vulnerabilities.
Testing
I would not recommend investing time in component testing with Angular. Writing unit tests for algorithm type code for example if you have a function that applies some formatting on numbers, that is something you may want to write unit tests for. For this, I recommend bootstrapping Jest onto your project.
When using jest for tests, keep in mind that jest runs on node and not in a browser, so any code that makes use of browser APIs that you would like to test would require mocks. Local storage is an example of an API that is implemented by browsers and not by node.
If you are not going to be writing component tests, it is best to remove all the dependencies for that (one less thing to worry about and keep up to date). Some dependencies to drop are jasmine, and karma including configuration files (karma.conf.js
, tsconfig.spec.json
etc) and any specs you may have already generated (*.spec.ts
). Make sure to also remove the test configuration in your angular.json
file.
If you are making use of the ng generate command make sure to use the
--skip-tests
flag if you are not making use of component tests.
Node version
As part of the upgrade do not forget to update your version of node. Make sure that you are working with a supported version of Node both locally and for your deployments. Check the official compatibility matrix.
⚠️ Do not run on an unsupported version of node. It may work now until you run into very weird bugs in the future. In those situations, you may not figure out that it is due to the node version and even if you do, now what? Bump down and regression test everything? Best to just avoid this. Also, make sure that the version of node used in production is the same one used by developers on their machines.
A good reason to upgrade Angular regularly is to keep up with an LTS version of Node. Node does however have very long maintenance windows. Check the official documentation here.
Scenic route
If you are making such a big change, this will no doubt be backed up by a regression test cycle. This is an opportunity to address any other low hanging-fruit.
Imports
Does your project have a lot of imports that walk up and down the folder tree like ../../../..
. These paths are not great as they tend to be long and do not do very well with moving files around, although most IDEs can handle this.
This is a nice time to address this by identifying the most commonly imported items in your project and creating aliases for those in your tsconfig.json
allowing us to simply something like ../../../components/..
to @components/..
Here is a sample configuration, this will differ depending on your folder structure.
{
"compilerOptions": {
"paths": {
"@services/*": [
"src/services/*"
],
"@models/*": [
"src/models/*"
],
"@environments/*": [
"src/environments/*"
],
"@store/*": [
"src/app/store/*"
],
"@modules/*": [
"src/app/modules/*"
],
"@shared/*": [
"src/app/modules/shared/*"
]
}
}
IDEs like VS Code will be aware of this configuration and will take this into account for auto imports.
Jest does not pull this information, so if you using Jest you will need to create module name mappings in the jest.config.js
, example
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleNameMapper: {
'^@services/(.*)$': '<rootDir>/src/services/$1',
'^@models/(.*)$': '<rootDir>/src/models/$1',
'^@environments/(.*)$': '<rootDir>/src/environments/$1',
'^@store/(.*)$': '<rootDir>/src/app/store/$1',
'^@modules/(.*)$': '<rootDir>/src/app/modules/$1',
'^@shared/(.*)$': '<rootDir>/src/app/modules/shared/$1',
},
};
Warnings
Look out for warnings that would be easy to address
- Build warnings
- Lint warnings
npm install
warnings
Some examples of things that may be shown as warnings after an upgrade
- Ambiguous CSS when working with flex layouts, for example,
start
andend
as opposed toflex-start
andflex-end
- Unsupported engines on doing
npm install
will warn you about the version of node being a mismatch to some packages - While not specific to the upgrade, it could be good to also address any vulnerabilities flagged by
npm install
. Consider updating all your packages and where that is not possible consider runningnpm audit --fix
to patch what would likely not result in breaking changes. - Angular will warn of the use of any CommonJS or AMD dependencies that can cause optimization bailouts and larger than necessary bundles. Angular can better do optimization during builds when working with ECMAScript modules. This warning will likely be due to dependencies and one common library that causes this is Lodash. In this case, it comes with
lodas-es
, that you can replace with to address this. - Due to updating TypeScript, you may start to see new warnings depending on what you have in your project.
In the example above of lodash
warnings, make sure to update the core and types packages to use lodash-es
and update all imports for example, import { isEmpty } from 'lodash';
becomes import { isEmpty } from 'lodash-es';
. If you are making use of jest, you will need to provide a module name mapper in your jest.config.js
/** @type {import('tsjest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleNameMapper: {
'^lodash-es$': 'lodash'
},
};
Linting
If you are modernizing your Angular SPA project, if you are not already working with eslint, I would highly recommend it and if you already are, this is also a good time to review your linting rules. If you are able to, upgrade to the highest eslint version that is supported for the version of Angular you have targeted.
Here is a simple configuration as an example for the latest eslint that now makes use of eslint.config.js.
import path from "node:path";
import { fileURLToPath } from "node:url";
import eslint from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: eslint.configs.recommended,
allConfig: eslint.configs.all
});
export default [{
ignores: ["projects/**/*", "coverage/**/*", "**/generated.ts"],
}, ...compat.extends(
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates",
).map(config => ({
...config,
files: ["**/*.ts"],
})), {
files: ["**/*.ts"],
linterOptions: {
reportUnusedDisableDirectives: "off"
},
languageOptions: {
ecmaVersion: 5,
sourceType: "script",
parserOptions: {
project: ["tsconfig.json"],
createDefaultProgram: true,
},
},
rules: {
"@angular-eslint/use-lifecycle-interface": ["error"],
"@angular-eslint/no-empty-lifecycle-method": "off",
"@angular-eslint/directive-selector": "off",
"@angular-eslint/component-selector": "off",
"@angular-eslint/no-output-on-prefix": "off",
"no-unused-vars": ["error", {
vars: "all",
args: "none",
ignoreRestSiblings: true,
}],
"no-console": "error",
"no-alert": "error",
"no-debugger": "error",
},
}, ...compat.extends("plugin:@angular-eslint/template/recommended").map(config => ({
...config,
files: ["**/*.html"],
})), {
files: ["**/*.html"],
rules: {},
}];
If you have a linting setup, make sure that your commands (ideally scripts in the
package.json
file pass the--cache
flag for faster runs). This will ensure that the linter only runs on files that have changed since the last run. Make sure to ignore the caching file from GIT.
If you have a linting setup, I highly recommend making use of lint-staged, to allow developers to run linting only for files that they have changed and are staged in GIT.
Keep your linting clean. Having tons of warnings that no one ever looks into is a nice way to slowly get the team to not care about linting. Either address all warnings, ignore existing warnings that are too difficult/risky to address or a combination of both.
Other cleanups
Also, consider other cleanups
- Remove any unused static assets to keep your bundles small.
- Consider your
node_modules
as well as your npm cache a mess right now and delete the folder, runnpm cache clean --force
and thennpm install