Angular Best Practices

Provides definitive guidelines for modern Angular development (2025), focusing on best practices for code structure, component architecture, state management with signals, performance, and type safety.

更新:2026-07-14

说明

# 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> ```

来源

https://github.com/sanjeed5/awesome-cursor-rules-mdc/blob/main/rules-mdc/angular.mdc

← 返回全部 Skill