# 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:** ```typescript // user.model-and-service.ts export interface User { id: string; name: string; }
@Injectable({ providedIn: 'root' }) export class UserService { // ... fetches users } ```
**✅ GOOD:** ```typescript // 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:** ```typescript // MyUserList.component.ts export class UserListComp { /* ... */ }
// authservice.ts export class AuthenticationService { /* ... */ } ```
**✅ GOOD:** ```typescript // 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:** ```typescript // 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:** ```typescript // 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:** ```typescript import { MyService } from './my.service';
@Component({...}) export class MyComponent { private myService: MyService;
constructor(myService: MyService) { this.myService = myService; } } ```
**✅ GOOD:** ```typescript 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:** ```typescript // 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:** ```typescript 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:** ```typescript // Manual subscription management ngOnInit() { this.userService.getUsers().subscribe(users => { this.users = users; }); }
ngOnDestroy() { // Forgot to unsubscribe, potential memory leak } ```
**✅ GOOD:** ```typescript 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:** ```html <!-- Using *ngIf --> <div *ngIf="isLoading">Loading...</div>
<!-- Using *ngFor --> <li *ngFor="let item of items; let i = index">{{ item.name }}</li> ```
**✅ GOOD:** ```html <!-- 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:** ```html <!-- Angular will re-render all items if the array reference changes --> @for (user of users) { <li>{{ user.name }}</li> } ```
**✅ GOOD:** ```html <!-- 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:** ```typescript // Loosely typed data fetchUsers().subscribe((data: any) => { console.log(data.users[0].firstName); // Prone to runtime errors }); ```
**✅ GOOD:** ```typescript 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:** ```typescript @Component({...}) export class MyComponent { title = 'My App'; // Can be reassigned anywhere private myService = inject(MyService); // Can be reassigned } ```
**✅ GOOD:** ```typescript @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:** ```html <!-- Custom button without proper ARIA roles --> <div (click)="doSomething()">Click Me</div> ```
**✅ GOOD:** ```html <!-- 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> ```