Angular: Advanced Tips and Best Practices for Experts and Beginners
Join the DZone community and get the full member experience.
Join For FreeI wrote this article and included some best practices, pitfalls, info, and ' what to look out for' when building an Angular application. A lot of these had been personal experiences and lessons learned the hard way. Also, it was really difficult to create this list, keep it short and simple since I wanted to add stuff for beginners and experts.
Try to follow what Angular recommends: don't do things your way
Since you're using Angular, you should follow what the creator suggests and not do things your way. This goes for most opinionated frameworks, not just Angular.
A MUST DO starting point is to read and follow the angular styleguide.
If you're new to Angular, start with tour of heroes
Its one of the best tutorials with very good documentation out there that helped me a lot back when I started with angular. The tour of heroes
Read the Documentation
The angular.io documentation is mature and almost complete. Besides that, you should check and be comfortable with Angular's Glossary.
Also, other things to check.
Consider using a UI Component library
You can choose between Angular Material, primeng, and many others that will make your life much easier
Use update.angular.io when upgrading version
Need to upgrade from older version to a newer and don’t know what will break, or what to change? No worries. Visit update.angular.io, and choose the version you currently have and the version you want to go, and follow the instructions. It also includes a guide for angular material.
Involve
Search for Angular meetups near your area.
Do you know Angular has its own Blog?
Check out top Angular events.
Also, don’t hesitate to open/report an issue when finding a problem regarding Angular itself or third party libraries. It will only make things better
Versioning/package.json, remove all caret ‘^’ and tilde ‘~’
If you want your app/library to be stable and not fail after a few months, you should highly consider removing all caret and tilde symbols from package.json.
Do not blindly accept automatic dependencies. You don’t want people knocking on your door when things go bad because of auto dependency update.
The web is evolving dramatically year after year. Try to run npm install on a 3-to-6-month-old project, and you get a warning plus deprecation messages not to mention there is a chance for a build failure.
If you want to have more control of your versions you can set this
npm config set save-prefix=''
Invest time staying up to date
It's recommended to invest some time once every 3 to 6 months and keep your dependencies up to date. Remember, a little warning or deprecation notice today will become 10 or 20 notices in the future and will bring higher problems.
If you are in the position where you running Angular 4, the latest version is 9, and you need to upgrade. Well bad news for you, it’s going to be a tough and unpleasant job. Because it’s not just the Angular you have to upgrade, it’s also all the dependencies that rely on.
Use types instead of Any
Avoid using any and prefer types. Use any if there is no other option or for special cases. That’s why you’re using Angular and TypeScript in the first place.
Know your package-lock.json
Remember your package-lock.json is the source of truth
package-lock.json lists the correct dependency versions your app is using. Refer to package-lock to know the exact version of a dependency your using.
For example, you may have this in your package.json ‘^1.1.1', but in runtime, you are using this ‘1.2.2’
Run npm ls module-name
to get the exact module version.
Package-lock is generated automatically from any operation that modifies package.json or node_modules tree, assuming you have node > 5.x.x.
Also, run npm ci
for clean install from package-lock
Watch out for dev, prod differences
Don’t take for granted that everything will be working just like development with prod build.
For example in development mode change detection runs twice. The second time it runs (after the first completes) if the bound values are different it produces the famous error: ExpressionChangedAfterItHasBeenCheckedError
.
Build-optimizer and vendor-chunk
Setting vendorChunk
to true in prod build the libraries will be placed in a separate vendor chunk.
Remember, if buildOptimizer
is true, set vendorChunk
to false. Using a build optimizer includes the library code in the bundle, resulting in a smaller size.
Use vendor chunk when:
- There is a lot of client updates without affecting much of the third-party dependencies/libraries
Do not use vendor chunk when:
- The bundle size difference is significantly larger.
- There are no frequent client updates.
Service in subModule Providers
Services are singleton in the module provided. Adding them to other module providers will create a new instance.
Consider using in Injectable providedIn root | any | platform
Prior to Angular 6 and later, we can use providedIn:'root'
in Injectable instead of importing the service in a module.
Also, Angular 9 introduced us with extra providedIn
options, any
, and platform
By declaring a providedIn
in Injectable service, we don’t have to import in any module. Instead, if we add the service in providers, we also have to import it, which will be included in the final bundle even if we don’t use it anywhere.
By using providedIn
, which is the official, preferred way, it enables Tree Shaking. With Tree Shaking, if the service isn’t used anywhere it won’t be bundled with the rest of the code, which leads to smaller bundle size and faster loading times
What about providedIn root | any | platform
options?
The time of writing this article, the official angular documentation of providedIn
only covers root
. So I will try to explain any
and platform
as simple as possible.
- root: One instance of the service for the whole application
- any: One instance for the eagerly loaded modules, different instance for every lazy module
- platform: One instance for all angular applications running on the same page (useful for shared service modules)
Lazy loading
Please lazy load your modules and don’t eager load them all at once. It makes a tremendous difference in loading time even for small applications. Consider eager loading for core modules and feature modules that required to start the app and do some initial interception.
I’ve seen big applications with all modules been eager load, and it some cases it exceeds 30 seconds to start.
You can check angular lazy-loading doc
Lazy loading loadChildren
from a secure place
If the resources and time are available you may consider store loadChildren
path in a secure place and fetch them after authorization.
Choose Angular i18n (even if it’s not mature yet) instead of ngx-translate
Yes, I know, you probably think that angular implementation of i18n sucks!. You have to create different builds for every language and to reload for every language change.
And you are right! It was a living hell for me and my team when working with these.
But…. keep in mind, Angular i18n is going through a lot of development effort and it is very complex. Angular is pushing it and eventually will become a lot better. Also, the main developer of ngx-translate is hired by Angular and working in i18n (source here). His statement is that ngx-translate is just a replacement for Angular until Angular i18n is completed, and at some point, ngx-translate will be deprecated.
Use trackBy
when you can
You rendered a collection with *ngFor
, and now you have to get new data from an HTTP request and reassign them to the same collection. This will cause Angular to remove all DOM elements associated with the data and re-render all DOM elements again. Having a large collection could cause a performance problem since DOM manipulations are expensive.
trackBy
comes to the rescue. By using trackBy
, rendering will happen only for the collection elements that have been modified(also new/removed).
Consider the following
xxxxxxxxxx
export class AppComponent {
//initial array
cars = [
{id:1,name:'Honda'},
{id:2,name:'Toyota'},
{id:3,name:'Hyundai'}];
setNewCars() {
this.cars = [
{id:1,name:'Honda'},
{id:2,name:'Mazda'},
{id:3,name:'Hyundai'},
{id:4,name:'BMW'}];
}
trackByFn(index,car){
return index;
}
}
xxxxxxxxxx
<ul>
<li *ngFor="let car of cars">{{car.name}}</li>
</ul>
<button (click)="setNewCars()">update cars</button>
By pressing the button, the whole ul
HTML block will be re-render.
Let’s add trackBy
by including trackByFn
and update the *ngFor
xxxxxxxxxx
<ul>
<li *ngFor="let car of cars; trackBy:trackByFn">{{car.name}}</li>
</ul>
Now, by pressing the button, the elements with a difference will be re-render. Which in our case are the updated item with id:2 and the new item with id:4.
Angular webworker has been marked as deprecated and may be removed at angular 10
Change detection strategy: OnPush
By default, Angular runs change detection on all components for every event that occurred. It’s the default ChangeDetectionStrategy.Default
. Having many components and a large number of elements could result in performance issues. To deal with that we can set ChangeDetectionStrategy.OnPush
on the components we want. For example.
xxxxxxxxxx
@Component({
changeDetection:ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
@Input
data;
}
By settings ChangeDetectionStrategy.OnPush
the ChildComponent
it only depends on the input values and it will only run checks if the input values change.
If the input is a value type it will run a check for the new value.
For a reference type value, a check will run only if is a difference between the old and new input reference.
Keep note to a case when we have a different input reference and change detection is not running.
xxxxxxxxxx
@Component({
changeDetection:ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
@Input
data;
setNewData(){
//Async get new data from http request
getData().then(d=>{
this.data = d;
//No UI update at this point
})
}
}
Having a similar code with above, you notice even if the input reference is changing, the component is not updating. That’s because there is not an event trigger to run the change detection in the first place.
The solution is to use inject ChangeDetectorRef
and programmatically trigger change detection.
xxxxxxxxxx
export class ChildComponent {
@Input
data;
constructor(private cd : ChangeDetectorRef){}
setNewData(){
//Async get new data from http request
getData().then(d=>{
this.data = d
this.cd.detectChanges()
})
}
}
Be careful with spamming events
As said above, every event triggers change detection. Be careful with global and window events that may cause unintended checks running on the whole app all the time
The notorious ExpressionChangedAfterItHasBeenCheckedError
error
If you see this error it means there is something wrong with your code.
In development mode, Angular runs two digests circles. The first time it evaluates the values and updates the DOM. If Angular is running on development mode, the circle runs a second time. In the second circle, Angular performs verification and compares the values against the first circle. If a difference is found, it produces the error.
That’s why this error means you have something very seriously wrong with your code. If the error wasn’t thrown, two things could happen. First, for the values to be the same between oldValues
and newValues
, it could lead to an infinite circle loop. And second, it could lead to an inconsistent model – view state.
A simple scenario that may cause the error
xxxxxxxxxx
AppComponent HTML
<childComponent [data]="originalData"</childComponent>
ChildComponent
ngOnInit(){
this.appComponent.originalData = "updatedValueData"
}
Tip: avoid manipulating a bound array directly in loop.
ViewChild
/ContentChild
availability
Both ViewChild
and ContentChild
are available from ngAfterViewInit
and after. ngAfterViewInit
is when the component view has been initialized.
ViewChildren
and ContentChildren
will return a QueryList
, while ViewChild
and ContentChild
will return the first element they matched.
Pure and impure Pipes
Pupe pipe is when angular executes the pipe only when input reference changes. By default, pipes are pure. Consider the following pure pipe.
xxxxxxxxxx
@Pipe({
name: 'jdmCars'
})
export class JdmCarsPipe implements PipeTransform {
transform(allCars: Car[]) {
return allCars.filter(car => car.isJdm);
}
}
Now if we add a new car somewhere in our component code…
xxxxxxxxxx
addNewJdmCar(){
this.cars.push({name:'Honda',isJdm:true});
}
Nothing happens. The view is not updating. This is because the car array reference is the same. If we want to see the changes, we have to make our pipe impure or re-reference(not recommended) the car array.
xxxxxxxxxx
@Pipe({
name: 'jdmCars',
pure: false
})
Base URL and Paths in tsconfig.json
Instead of heaving imports like this…
xxxxxxxxxx
import { MyService } from '../../services/MyService';
import { MyComponent } from '../../../../app/core/MyComponent';
Define the base paths you want in tsconfig.json
…
{
"compilerOptions":{
"baseUrl":"src",
"paths": {
"@app/*": ["src/core/*"],
"@assets/*": ["src/assets/*"],
"@service/*": ["src/services/*"]
}
}
}
//And use them
import { MyService } from '@service/MyService';
import { MyComponent } from '@app/MyComponent';
Will save you a lot of time in refactoring
Why not check out Redux
Depending on the situation and if state management of type undo/redo is needed, you could consider using redux for your next angular app. You could check out rxjs for Angular.
Published at DZone with permission of Avraam Piperidis, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments