
Angular is a powerful framework, but many developers struggle with it because of small mistakes made early in a project. These mistakes slowly affect performance, maintainability, and developer experience.
Based on real project experience, here are some common Angular mistakes and how you can avoid them.
Not Following a Proper Folder Structure
Many developers put everything inside one folder. Components, services, and models get mixed together. This becomes hard to manage as the app grows.
How to avoid it:
- Organize by feature, not by file type
- Keep each feature in its own folder
- Separate core, shared, and feature modules
- Use an index.ts (barrel) file for clean exports
Example folder structure (feature-based)
src/app/
├── core/
│ ├── auth/
│ └── http/
├── shared/
│ ├── ui/
│ └── constants/
└── features/
├── blog/
│ ├── pages/
│ ├── components/
│ ├── services/
│ └── models/
└── projects/
├── pages/
├── components/
└── services/A clean structure makes your project easier to scale and understand.
Overusing Logic Inside Components
Angular components should handle UI logic only. Many developers add API calls, heavy calculations, and business logic directly inside components.
How to avoid it:
- Move business logic to services
- Keep components simple and readable
- Use services for API calls and shared logic
- Use pure pipes for simple template transformations
Example: move API calls to a service
// blog.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface BlogPostDto {
id: string;
title: string;
}
@Injectable({ providedIn: 'root' })
export class BlogService {
constructor(private readonly http: HttpClient) {}
getPosts(): Observable<readonly BlogPostDto[]> {
return this.http.get<readonly BlogPostDto[]>('/api/posts');
}
}
// blog.component.ts
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { BlogService, type BlogPostDto } from './blog.service';
@Component({ /* ... */ })
export class BlogComponent {
readonly posts$: Observable<readonly BlogPostDto[]> =
this.blogService.getPosts();
constructor(private readonly blogService: BlogService) {}
}This makes components easier to test and reuse.
Ignoring Unsubscribing From Observables
One of the most common Angular mistakes is forgetting to unsubscribe from observables. This leads to memory leaks and performance issues.
How to avoid it:
- Use the async pipe when possible
- Unsubscribe manually in ngOnDestroy
- Use operators like takeUntil with a subject
- Use takeUntilDestroyed (Angular 16+)
Example: safe subscription (Angular 16+)
import { Component, DestroyRef, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({ /* ... */ })
export class ExampleComponent {
private readonly destroyRef = inject(DestroyRef);
constructor(private readonly route: ActivatedRoute) {
this.route.paramMap
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(params => {
// safe: auto-unsubscribes on destroy
console.log(params.get('id'));
});
}
}Example: async pipe (no manual subscribe)
<!-- component.html -->
<li *ngFor="let post of posts$ | async">{{ post.title }}</li>Managing subscriptions properly keeps your app fast and stable.
Using Any Type Everywhere
Using any removes the biggest advantage of Angular and TypeScript. It hides errors and makes code unsafe.
How to avoid it:
- Define proper interfaces and models
- Use strict typing
- Avoid any unless absolutely necessary
- Use unknown if the type is truly unknown
Example: use a typed interface (no any)
export interface User {
id: string;
name: string;
email: string;
}
// Typed HTTP response
getUser(id: string) {
return this.http.get<User>(`/api/users/${id}`);
}Type safety helps catch bugs early and improves code quality.
Poor State Management
Handling state inside multiple components without a plan leads to messy code and bugs.
How to avoid it:
- Use services for shared state
- Use RxJS subjects (BehaviorSubject) wisely
- Use Signals for UI-local state (Angular 16+)
- For large apps, use NgRx or NGXS
Clear state management keeps data flow predictable.
Not Optimizing Change Detection
Angular apps can become slow if change detection is not handled correctly, especially in large applications.
How to avoid it:
- Use OnPush change detection where possible
- Avoid unnecessary function calls in templates
- Use pure pipes for calculations
- Keep templates clean and simple
Example: OnPush + avoid calling functions in templates
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
@Component({
selector: 'app-user-card',
templateUrl: './user-card.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
@Input({ required: true }) name!: string;
}Template example (bind to values, not functions)
<!-- Good -->
<h3>{{ name }}</h3>
<!-- Avoid -->
<!-- <h3>{{ getName() }}</h3> -->Small optimizations make a big difference in performance.
Skipping Code Reusability
Copy pasting components or logic is a common habit, but it increases bugs and maintenance effort.
How to avoid it:
- Create reusable components
- Use shared modules or standalone components wisely
- Write generic components with Content Projection
- Use Directive for shared behavior
Example: a reusable UI component
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-section-title',
template: '<h2 class="section-title">{{ text }}</h2>',
standalone: true,
})
export class SectionTitleComponent {
@Input({ required: true }) text!: string;
}Reusable code saves time and improves consistency.
Over-complicating RxJS Streams
RxJS is powerful, but nested subscriptions or giant operators chains can be impossible to debug.
Pro-tip: Use descriptive operators and avoid "Nested Subscriptions" (Subscription inside another subscribe). Use flattening operators like switchMap instead.
Good: Flattening with switchMap
// Avoid this:
this.user$.subscribe(user => {
this.orderService.get(user.id).subscribe(orders => ...)
});
// Do this:
this.orders$ = this.user$.pipe(
switchMap(user => this.orderService.get(user.id))
);Not Using trackBy with *ngFor
When you update a list, Angular re-renders the entire DOM by default. trackBy helps Angular identify which items changed and only update those.
Example: trackBy for performance
<!-- component.html -->
<li *ngFor="let item of items; trackBy: trackById">
{{ item.name }}
</li>Component side
trackById(index: number, item: any): string {
return item.id;
}Direct DOM Manipulation
Manipulating the DOM directly using document.querySelector or native elements breaks Angular's abstraction and can lead to bugs with SSR or testing.
Instead, use Renderer2, ElementRef (carefully), or template variables.
Final Thoughts
Angular is not hard, but it requires discipline. Most problems come from rushed decisions and lack of structure.
If you focus on clean architecture, proper typing, and good practices early, Angular becomes a joy to work with.
If you found this useful, share it with another Angular developer.
Stay tuned for more real-world frontend development insights.