Angular Best Practices
This guide outlines the definitive best practices for Angular development, ensuring consistency, maintainability, and optimal performance. Adhere to these rules strictly.
1. Code Organization and Structure
1.1 One Concept Per File (SRP)
Rule: Each file must contain a single Angular concept (e.g., one component, one directive, one pipe, one service, one interface).
Explanation: This enforces the Single Responsibility Principle (SRP), improving modularity, readability, and testability. Exceptions are rare and only for tightly coupled, non-exported elements.
❌ BAD:
// user.model-and-service.ts
export interface User { id: string; name: string; }
@Injectable({ providedIn: 'root' })
export class UserService {
// ... fetches users
}
✅ GOOD:
// user.model.ts
export interface User { id: string; name: string; }
// user.service.ts
@Injectable({ providedIn: 'root' })
export class UserService {
// ... fetches users
}
1.2 Consistent Naming Conventions
Rule: Follow kebab-case for file names (feature-name.type.ts) and PascalCase for class names. Use standard type suffixes.
Explanation: Consistent naming makes files easy to locate and understand at a glance.
❌ BAD:
// MyUserList.component.ts
export class UserListComp { /* ... */ }
// authservice.ts
export class AuthenticationService { /* ... */ }
✅ GOOD:
// user-list.component.ts
export class UserListComponent { /* ... */ }
// auth.service.ts
export class AuthService { /* ... */ }
2. Component Architecture
2.1 Standalone Components by Default
Rule: Always create standalone components, directives, and pipes. Avoid NgModules for new features unless strictly necessary for legacy integration or specific library patterns.
Explanation: Standalone components simplify the Angular mental model, reduce boilerplate, and improve tree-shaking.
❌ BAD:
// app.module.ts
@NgModule({
declarations: [MyComponent],
imports: [CommonModule],
// ...
})
export class AppModule { }
// my.component.ts
@Component({
selector: 'app-my',
template: `<p>My Component</p>`,
styleUrls: ['./my.component.css'],
})
export class MyComponent { }
✅ GOOD:
// my.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; // Import dependencies directly
@Component({
selector: 'app-my',
standalone: true, // Always standalone
imports: [CommonModule],
template: `<p>My Component</p>`,
styleUrls: ['./my.component.css'],
})
export class MyComponent { }
2.2 Prefer inject for Dependency Injection
Rule: Use the inject function for all dependency injection, especially in field initializers. Avoid constructor injection unless required for specific lifecycle hooks or inheritance patterns.
Explanation: inject offers greater flexibility, better tree-shaking, and allows injection outside of constructors.
❌ BAD:
import { MyService } from './my.service';
@Component({...})
export class MyComponent {
private myService: MyService;
constructor(myService: MyService) {
this.myService = myService;
}
}
✅ GOOD:
import { inject } from '@angular/core';
import { MyService } from './my.service';
@Component({...})
export class MyComponent {
// Use a private readonly field for injected services
private readonly myService = inject(MyService);
// You can also inject in functions if needed (e.g., providers)
// readonly myOtherService = someFactoryFn(inject(AnotherService));
}
3. State Management
3.1 Prioritize Signals for Component State
Rule: Use Angular Signals for all local component state and simple derived state. Reserve RxJS for complex asynchronous data streams, side effects, and inter-component communication.
Explanation: Signals provide a reactive, performant, and simpler way to manage state, optimizing change detection automatically.
❌ BAD:
// Using plain properties and relying on Zone.js for change detection
@Component({...})
export class MyComponent {
count = 0;
items: string[] = [];
addItem(item: string) {
this.items.push(item); // Direct mutation, less reactive
}
}
✅ GOOD:
import { Component, signal, computed } from '@angular/core';
@Component({...})
export class MyComponent {
readonly count = signal(0); // Primitive signal
readonly items = signal<string[]>([]); // Array signal
// Derived state with computed
readonly hasItems = computed(() => this.items().length > 0);
increment() {
this.count.update(value => value + 1);
}
addItem(item: string) {
this.items.update(currentItems => [...currentItems, item]); // Immutable update
}
}
3.2 RxJS for Asynchronous Streams and Effects
Rule: Leverage RxJS for handling HTTP requests, complex event streams, and orchestrating side effects. Always manage subscriptions to prevent memory leaks.
Explanation: RxJS is powerful for reactive programming with asynchronous data. Use toSignal or takeUntilDestroyed for automatic unsubscription.
❌ BAD:
// Manual subscription management
ngOnInit() {
this.userService.getUsers().subscribe(users => {
this.users = users;
});
}
ngOnDestroy() {
// Forgot to unsubscribe, potential memory leak
}
✅ GOOD:
import { Component, inject, DestroyRef } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop'; // For converting RxJS to Signal
import { UserService } from './user.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { tap } from 'rxjs';
@Component({...})
export class MyComponent {
private readonly userService = inject(UserService);
private readonly destroyRef = inject(DestroyRef);
// Convert an RxJS Observable to a Signal, automatically unsubscribes
readonly users = toSignal(this.userService.getUsers(), { initialValue: [] });
// For effects or more complex streams, manage with takeUntilDestroyed
constructor() {
this.userService.someAction$
.pipe(
tap(action => console.log('Action:', action)),
takeUntilDestroyed(this.destroyRef) // Automatic unsubscription
)
.subscribe();
}
}
4. Performance Considerations
4.1 Use Built-in Control Flow
Rule: Discontinue the use of structural directives (*ngIf, *ngFor, *ngSwitch). Always use Angular's new built-in control flow syntax.
Explanation: The new control flow (@if, @for, @switch) offers improved performance, better type checking, and a more intuitive syntax.
❌ BAD:
<!-- Using *ngIf -->
<div *ngIf="isLoading">Loading...</div>
<!-- Using *ngFor -->
<li *ngFor="let item of items; let i = index">{{ item.name }}</li>
✅ GOOD:
<!-- Using @if -->
@if (isLoading) {
<div>Loading...</div>
} @else {
<div>Data loaded!</div>
}
<!-- Using @for with track -->
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
} @empty {
<p>No items found.</p>
}
4.2 Always Use track Key in @for
Rule: Always provide a track key in @for loops. Use a unique, primitive property of the object (e.g., item.id). If iterating over primitives, use the value directly.
Explanation: The track key helps Angular optimize rendering by identifying unique items, preventing unnecessary DOM re-renders and improving performance.
❌ BAD:
<!-- Angular will re-render all items if the array reference changes -->
@for (user of users) {
<li>{{ user.name }}</li>
}
✅ GOOD:
<!-- Optimized re-rendering based on user.id -->
@for (user of users; track user.id) {
<li>{{ user.name }}</li>
}
<!-- For primitive arrays, track the value itself -->
@for (name of names; track name) {
<li>{{ name }}</li>
}
5. Type Safety
5.1 Define Interfaces for Data Structures
Rule: Always define TypeScript interfaces or types for all data structures, especially those received from APIs or used in models.
Explanation: Interfaces provide strong type checking, improve code readability, and catch errors early.
❌ BAD:
// Loosely typed data
fetchUsers().subscribe((data: any) => {
console.log(data.users[0].firstName); // Prone to runtime errors
});
✅ GOOD:
export interface User {
id: string;
firstName: string;
lastName: string;
email: string;
}
fetchUsers().subscribe((data: User[]) => {
console.log(data[0].firstName); // Type-safe access
});
5.2 Default to readonly Properties
Rule: Declare component properties and injected services as readonly where their values are not intended to be reassigned after initialization.
Explanation: readonly encourages immutability, prevents accidental reassignments, and makes state flow more predictable.
❌ BAD:
@Component({...})
export class MyComponent {
title = 'My App'; // Can be reassigned anywhere
private myService = inject(MyService); // Can be reassigned
}
✅ GOOD:
@Component({...})
export class MyComponent {
readonly title = 'My App'; // Immutable after initialization
private readonly myService = inject(MyService); // Immutable reference to service
}
6. Accessibility (A11y)
6.1 Leverage Angular Aria and CDK
Rule: Integrate Angular Aria and the Component Dev Kit (CDK) for building accessible UI components. Always provide appropriate ARIA attributes and semantic HTML.
Explanation: Angular's built-in tools help ensure your applications are usable by everyone, including those with disabilities.
❌ BAD:
<!-- Custom button without proper ARIA roles -->
<div (click)="doSomething()">Click Me</div>
✅ GOOD:
<!-- Semantic button with ARIA attributes for accessibility -->
<button type="button" (click)="doSomething()" aria-label="Perform action">Click Me</button>
<!-- Using CDK for accessible interactions, e.g., cdkMenu -->
<button cdkMenuTrigger>Open Menu</button>
<div cdkMenu>
<button cdkMenuItem>Item 1</button>
</div>