DEV Community

Cover image for 5 Common Angular Pitfalls and How to Avoid Them
David Brooks
David Brooks

Posted on

5 Common Angular Pitfalls and How to Avoid Them

Beware

Angular is a powerful framework, but even experienced developers can fall into common traps that hurt performance, maintainability, or readability. Falling victim to these small issues can quietly fuel that all-too-familiar developer ailment: imposter syndrome.

In this article, I’ll walk through five common Angular pitfalls I see in real-world applications and show how to avoid them with practical examples and best practices.


1. Misusing Lifecycle Hooks (ngOnInit, ngOnChanges)

The problem:

Many developers overuse ngOnInit or put heavy logic in ngOnChanges, causing unnecessary re-renders or complicated debugging.

The solution

  • Keep lifecycle hooks focused on initialization or change detection that actually depends on input changes
  • Move business logic to services instead of components

Bad example:

ts 


ngOnInit() {
  this.loadData();
  this.processData(); // heavy computation here
}
Enter fullscreen mode Exit fullscreen mode

Better example

ts


ngOnInit() {
  this.loadData();
}

private loadData() {
  this.dataService.getData().subscribe(data => {
    this.processData(data);
  });
}
Enter fullscreen mode Exit fullscreen mode

Why it matters:

Separating concerns keeps components lightweight, testable, and easier to maintain.

2. Overusing Two-Way Binding ([(ngModel)])

The problem

Two-way binding is convenient, but overusing [(ngModel)]—especially in larger forms—can lead to hidden side effects and messy validation logic.

The solution

  • Use Reactive Forms for anything non-trivial
  • Reserve ngModel for very simple inputs

Reactive forms example

ts


form = new FormGroup({
  name: new FormControl('', Validators.required),
  email: new FormControl('', [
    Validators.required,
    Validators.email
  ])
});
Enter fullscreen mode Exit fullscreen mode
html


<form [formGroup]="form">
  <input formControlName="name" />
  <input formControlName="email" />
</form>
Enter fullscreen mode Exit fullscreen mode

Why it matters:

Reactive forms make validation, testing, and debugging much more predictable as your app grows.

3. Improper State Management (BehaviorSubject vs Signals)

The problem

Many Angular applications rely heavily on BehaviorSubject for all state, even when the state is simple. This can lead to unnecessary RxJS boilerplate and more complex code than needed.

The solution

  • Use Signals (Angular 16+) for simple reactive state
  • Use BehaviorSubject or NgRx for shared or complex state
  • Keep state logic in services, not components

Signals example

ts 


import { signal } from '@angular/core';

export class CounterService {
  counter = signal(0);

  increment() {
    this.counter.update(value => value + 1);
  }
}
Enter fullscreen mode Exit fullscreen mode

Why it matters:

If you’re already deep into NgRx, Signals won’t replace it—but they’re a great fit for local or service-level state.

Signals provide clean, readable reactivity without the overhead of observables when your state doesn’t require complex streams or operators.

4. Forgetting trackBy in *ngFor

The problem

When rendering lists without a trackBy function, Angular destroys and recreates DOM elements whenever the list changes—even if only one item was updated. This can cause unnecessary re-renders and performance issues, especially with larger lists.

The solution

  • Always provide a trackBy function when iterating over collections

Good practice example

html


<li *ngFor="let item of items; trackBy: trackById">
  {{ item.name }}
</li>
Enter fullscreen mode Exit fullscreen mode
ts


trackById(index: number, item: { id: number }) {
  return item.id;
}
Enter fullscreen mode Exit fullscreen mode

Why it matters:

Using trackBy ensures Angular only updates the elements that actually changed, significantly improving rendering performance.

5. Inefficient Change Detection

The problem

Default change detection can cause unnecessary checks across the component tree, which leads to degraded performance in larger applications.

The solution

  • Use ChangeDetectionStrategy.OnPush
  • Pass immutable data to components
  • Avoid unnecessary template bindings

Example

ts


import { ChangeDetectionStrategy, Component, Input } from '@angular/core';

@Component({
  selector: 'app-user-card',
  templateUrl: './user-card.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserCardComponent {
  @Input() user!: User;
}
Enter fullscreen mode Exit fullscreen mode

Why it matters:

OnPush dramatically reduces unnecessary change detection cycles and helps keep your UI fast and responsive.


Key takeaways:

  • Avoid common lifecycle hook misuse
  • Prefer Reactive Forms over excessive ngModel
  • Use Signals for simple state
  • Optimize lists with trackBy
  • Boost performance with OnPush change detection

About the Author

I’m a full-stack developer with a strong focus on frontend development, specializing in Angular and technical writing. I write practical, example-driven guides based on real-world experience.

If you’re looking for a freelance technical writer, feel free to connect with me on LinkedIn.

Top comments (0)