Subtle Behaviors and Oddities in Angular
blogApril 17, 2025

Subtle Behaviors and Oddities in Angular

From Version 16 to 18

Article presentation

What you'll learn from this article:  

The real differences between [(ngModel)] and (ngModelChange) 

Common pitfalls in data bindings 

Are even-numbered Angular versions more stable than odd ones? 

Behaviors introduced or removed between versions 

Hidden issues you can avoid 

Best practices for version migration 

Real-world bug examples and fixes 


The Difference Between [(ngModel)] and (ngModelChange) 

There are two ways to react to input changes: 

<!-- Option 1: Two-way binding --> 

<input [(ngModel)]='value' /> 

 

<!-- Option 2: Separate event + property binding --> 

<input [ngModel]='value' (ngModelChange)='onChange($event)' /> 

 

Common Pitfall: 

<!--Incorrect order --> 

<input (ngModelChange)='onChange($event)' [(ngModel)]='value' /> 

In this case, onChange() is triggered before the value is actually updated. The function receives the new value, but the component's property hasn’t been synced yet. 

Correct usage: 

<input [(ngModel)]='value' (ngModelChange)='onChange($event)' /> 

Now, [(ngModel)] updates the value before ngModelChange is emitted. 


Other Common Angular Pitfalls 

ngIf + @ViewChild: 

If an element is conditionally rendered using *ngIf, the reference retrieved using @ViewChild may be undefined during ngOnInit. 

 Solution: Use ngAfterViewInit or set static: false. 

ngOnChanges vs ngDoCheck: 

ngOnChanges only works with @Input() values received from a parent. 

ngDoCheck can detect internal state changes but requires custom comparison logic. 

FormControl and focus issues: 

Setting a value on a FormControl right after creation might cause focus loss. 

Solution: Use setTimeout() or ngAfterViewInit for more predictable behavior. 

trackBy in *ngFor: 

Without trackBy, Angular re-creates the entire DOM for each item. 

With trackBy, only changed elements are re-rendered:  

<li *ngFor='let item of items; trackBy: trackById'>{{ item.name }}</li> 

trackById(index: number, item: any) { 

  return item.id; 

Angular Even vs Odd Versions — Myth or Reality? 

Version> Stability> Notes 

v. 16    > Very stable> Full Standalone API support introduced 

v. 17    >️ Experimental> Control flow syntax introduced (@if, @for) 

v. 18    > Consolidated> Template hydration + control flow stabilization 

Conclusion: 

Even versions tend to be more stable and production-ready. Odd versions often bring experimental features that get refined in the next release. 


Hidden Issues Between Versions 

Angular 16: 

Fully introduces standalone components, but many third-party libraries lack support. 

Breaking changes related to HttpClient injection. 

Angular 17: 

New control flow syntax (@if, @for, @defer) is not backward-compatible. 

zone.js changes can break legacy components. 

Angular 18: 

Template hydration can cause issues with SSR if the DOM isn’t in sync. 

Strict mode adds even more template validation constraints. 

Best Practices for Migrating Between Versions 

Test incrementally using ng update --next on a separate branch 

Check official changelogs for breaking changes 

Use ng update for automated upgrade suggestions 

Ensure all third-party libraries are compatible with the new version 

Gradually enable strict mode to catch issues early 


Real Bug Example and Fix 

Scenario: 

After updating to Angular 17, an app using [(ngModel)] on a custom <select> started throwing ExpressionChangedAfterItHasBeenCheckedError. 

Cause: 

The binding behavior of ngModel changed subtly and now syncs differently when combined with ngIf. 

Solution: 

Refactor to use ReactiveForms and remove the ngIf condition from around the select. 

 this.form = this.fb.group({ 

  tip: [null] 

}); 


<select [formControl]='form.controls["tip"]'> 

  <option *ngFor='let tip of tipuri' [value]='tip'>{{ tip }}</option> 

</select> 

Angular Version Performance Differences 

Performance Benchmarking: 


Version| Cold Start (Dev) | Build Time (Prod) | Bundle Size | Notes 

v. 16    | 3.1s                    | 12.4s                     | 358KB        | Solid, stable performance 

v. 17    | 2.7s                    | 10.9s                     | 342KB        |Control flow optimizations shrink size 

v. 16    | 3.1s                    | 12.4s                     | 358KB        | Solid, stable performance vements 


Interpretation: 

Angular 18 shows the best compile time and smallest bundle size. 

Improvements come from better control flow, hydration support, and Ivy runtime enhancements. 

Performance differences are more noticeable in large apps (>500 components), minimal in smaller apps. 


Final Thoughts & Recommendations 

- Always place (ngModelChange) after [(ngModel)] 

- Avoid jumping from Angular 14 directly to 18 without proper testing 

- Always consult the official changelog and documentation 

- Use trackBy, ngAfterViewInit, and strictTemplates for more stable, performant code 

- Migrate incrementally with automated tests and clean refactoring 

- Test odd-numbered versions in internal projects or PoCs, not in production 


Author’s Opinion 

I've been working with Angular since version 8 and I can say that I've seen all the maturing phases of this framework. Personally, I prefer adopting even-numbered versions in real-world projects because they offer a better balance between innovation and reliability. Odd-numbered versions are exciting due to their cutting-edge features, but I tend to explore them in internal tools or proof-of-concept apps. 

Angular remains a powerful framework, but it must be used with caution and deep understanding—especially when it comes to migration strategies and subtle binding behaviors like ngModelChange.