web-framework-angular-standalone

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Angular Standalone Components

Angular 独立组件

Quick Guide: Components are standalone by default in Angular 19. Use
signal()
,
computed()
,
effect()
,
linkedSignal()
for reactive state. Use
input()
,
output()
,
model()
for component communication. Use
@if
,
@for
,
@switch
,
@defer
for template control flow. Use
inject()
for dependency injection. Use
resource()
for async data fetching.

<critical_requirements>
快速指南: Angular 19 中组件默认是独立的。使用
signal()
computed()
effect()
linkedSignal()
实现响应式状态。使用
input()
output()
model()
进行组件通信。使用
@if
@for
@switch
@defer
实现模板控制流。使用
inject()
进行依赖注入。使用
resource()
获取异步数据。

<critical_requirements>

CRITICAL: Before Using This Skill

重要提示:使用本技能前须知

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type
, named constants)
(You MUST write standalone components (the default in Angular 19) - only specify
standalone: false
when intentionally using NgModules)
(You MUST use
input()
,
output()
,
model()
functions instead of
@Input()
,
@Output()
decorators)
(You MUST use
inject()
function for dependency injection, NOT constructor injection)
(You MUST use
@if
,
@for
,
@switch
control flow blocks, NOT
*ngIf
,
*ngFor
,
*ngSwitch
)
(You MUST use
track
expression in ALL
@for
loops)
(You MUST use
linkedSignal()
instead of manual signal synchronization for dependent writable state)
</critical_requirements>

Auto-detection: Angular component, standalone component, signal, computed, effect, linkedSignal, resource, rxResource, httpResource, input(), output(), model(), @if, @for, @switch, @defer, inject(), provideRouter, afterRenderEffect
When to use:
  • Building Angular 17-19 components with standalone architecture
  • Implementing reactive state with signals
  • Creating component communication with signal-based inputs/outputs
  • Setting up routing with standalone components
  • Lazy loading components with
    @defer
    or
    loadComponent
  • Fetching async data with
    resource()
    ,
    rxResource()
    , or
    httpResource()
Key patterns covered:
  • Standalone component architecture (default in Angular 19)
  • Signals for reactive state (signal, computed, effect, linkedSignal)
  • Resource API for async data (resource, rxResource, httpResource) [experimental]
  • Signal-based inputs and outputs (input, output, model)
  • Control flow blocks (@if, @for, @switch, @defer)
  • Dependency injection with inject()
  • Routing with provideRouter and loadComponent
  • DOM effects with afterRenderEffect()
When NOT to use:
  • Legacy Angular projects that must use NgModules (consult migration guides)
  • Simple scripts without Angular framework
Detailed Resources:
  • For core code examples, see examples/core.md
  • For advanced patterns (@defer, DI config, model(), RxJS interop), see examples/
  • For decision frameworks and anti-patterns, see reference.md

<philosophy>
所有代码必须遵循 CLAUDE.md 中的项目规范(短横线命名、命名导出、导入排序、
import type
、命名常量)
(必须编写独立组件——Angular 19 默认模式——仅在有意使用 NgModules 时才指定
standalone: false
(必须使用
input()
output()
model()
函数,而非
@Input()
@Output()
装饰器)
(必须使用
inject()
函数进行依赖注入,而非构造函数注入)
(必须使用
@if
@for
@switch
控制流块,而非
*ngIf
*ngFor
*ngSwitch
(所有
@for
循环必须使用
track
表达式)
(对于依赖型可写状态,必须使用
linkedSignal()
而非手动同步信号)
</critical_requirements>

自动检测项: Angular 组件、独立组件、signal、computed、effect、linkedSignal、resource、rxResource、httpResource、input()、output()、model()、@if、@for、@switch、@defer、inject()、provideRouter、afterRenderEffect
适用场景:
  • 基于独立架构构建 Angular 17-19 组件
  • 使用 signals 实现响应式状态
  • 基于 signal 的输入/输出创建组件通信
  • 为独立组件配置路由
  • 使用
    @defer
    loadComponent
    懒加载组件
  • 使用
    resource()
    rxResource()
    httpResource()
    获取异步数据
涵盖的核心模式:
  • 独立组件架构(Angular 19 默认)
  • 用于响应式状态的 Signals(signal、computed、effect、linkedSignal)
  • 用于异步数据的 Resource API(resource、rxResource、httpResource)[实验性]
  • 基于 Signal 的输入和输出(input、output、model)
  • 控制流块(@if、@for、@switch、@defer)
  • 使用 inject() 进行依赖注入
  • 使用 provideRouter 和 loadComponent 实现路由
  • 使用 afterRenderEffect() 处理 DOM 副作用
不适用场景:
  • 必须使用 NgModules 的遗留 Angular 项目(参考迁移指南)
  • 不依赖 Angular 框架的简单脚本
详细资源:
  • 核心代码示例请查看 examples/core.md
  • 高级模式(@defer、DI 配置、model()、RxJS 互操作)请查看 examples/
  • 决策框架与反模式请查看 reference.md

<philosophy>

Philosophy

设计理念

Angular 17-19 embraces a standalone-first architecture that eliminates NgModule boilerplate. In Angular 19,
standalone: true
is the default
- you only need to specify
standalone: false
for NgModule components. Signals provide synchronous, fine-grained reactivity for predictable state management. The new control flow syntax (
@if
,
@for
,
@switch
,
@defer
) is built into templates without imports, offering better type narrowing and smaller bundles. Components should be self-contained, lazy-loadable units that declare their own dependencies.
Angular's Four Pillars (17-19):
  1. Standalone by Default - Components, directives, and pipes are standalone by default in v19
  2. Signal-Based Reactivity - Synchronous, memoized, fine-grained change detection with
    signal()
    ,
    computed()
    ,
    linkedSignal()
  3. Built-In Control Flow - Template syntax that requires no imports and optimizes at build time
  4. Resource API - Experimental async data fetching that integrates with signals (
    resource()
    ,
    rxResource()
    ,
    httpResource()
    in 19.2)
</philosophy>
<patterns>
Angular 17-19 采用独立优先架构,消除了 NgModule 的冗余代码。在 Angular 19 中,
standalone: true
是默认配置
——仅在使用 NgModule 组件时才需要指定
standalone: false
。Signals 提供同步、细粒度的响应式能力,实现可预测的状态管理。新的控制流语法(
@if
@for
@switch
@defer
)内置在模板中无需导入,提供更优的类型收窄和更小的打包体积。组件应是自包含、可懒加载的单元,自行声明依赖项。
Angular 17-19 的四大支柱:
  1. 默认独立 - v19 中组件、指令、管道默认是独立的
  2. 基于 Signal 的响应式 - 使用
    signal()
    computed()
    linkedSignal()
    实现同步、缓存、细粒度变更检测
  3. 内置控制流 - 模板语法无需导入,构建时自动优化
  4. Resource API - 实验性异步数据获取方案,与 signals 集成(19.2 版本中的
    resource()
    rxResource()
    httpResource()
</philosophy>
<patterns>

Core Patterns

核心模式

Pattern 1: Standalone Component Structure

模式 1:独立组件结构

All Angular 17-19 components use
standalone: true
(the default in Angular 19) and declare their own imports.
typescript
// user-card.component.ts
import { Component, input, output } from "@angular/core";
import { DatePipe } from "@angular/common";

export type User = {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
};

@Component({
  selector: "app-user-card",
  standalone: true,
  imports: [DatePipe],
  template: `
    <article class="user-card">
      <h2>{{ user().name }}</h2>
      <p>{{ user().email }}</p>
      <time>Joined: {{ user().createdAt | date: "mediumDate" }}</time>
      <button (click)="edit.emit(user())">Edit</button>
    </article>
  `,
})
export class UserCardComponent {
  // Signal-based input (required)
  user = input.required<User>();

  // Signal-based output
  edit = output<User>();
}
Why good: standalone: true eliminates NgModule boilerplate, imports array declares dependencies explicitly for tree-shaking, signal-based input() and output() provide type-safe reactive communication, template is colocated for readability
typescript
// BAD - Legacy patterns
@Component({
  selector: "app-user-card",
  template: `...`,
})
export class UserCardComponent {
  @Input() user!: User; // Legacy decorator
  @Output() edit = new EventEmitter<User>(); // Legacy EventEmitter
}
Why bad: @Input decorator lacks signal reactivity, EventEmitter is less type-safe than output(), non-null assertion (!) hides potential undefined errors, no imports array means dependencies aren't explicit

所有 Angular 17-19 组件使用
standalone: true
(Angular 19 默认)并自行声明导入项。
typescript
// user-card.component.ts
import { Component, input, output } from "@angular/core";
import { DatePipe } from "@angular/common";

export type User = {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
};

@Component({
  selector: "app-user-card",
  standalone: true,
  imports: [DatePipe],
  template: `
    <article class="user-card">
      <h2>{{ user().name }}</h2>
      <p>{{ user().email }}</p>
      <time>Joined: {{ user().createdAt | date: "mediumDate" }}</time>
      <button (click)="edit.emit(user())">Edit</button>
    </article>
  `,
})
export class UserCardComponent {
  // Signal-based input (required)
  user = input.required<User>();

  // Signal-based output
  edit = output<User>();
}
优势: standalone: true 消除了 NgModule 冗余代码,imports 数组显式声明依赖以支持摇树优化,基于 signal 的 input() 和 output() 提供类型安全的响应式通信,模板与组件代码同位置提高可读性
typescript
// BAD - Legacy patterns
@Component({
  selector: "app-user-card",
  template: `...`,
})
export class UserCardComponent {
  @Input() user!: User; // Legacy decorator
  @Output() edit = new EventEmitter<User>(); // Legacy EventEmitter
}
劣势: @Input 装饰器缺乏 signal 响应式能力,EventEmitter 比 output() 类型安全性更低,非空断言 (!) 隐藏潜在的未定义错误,无 imports 数组意味着依赖不明确

Pattern 2: Signals for Reactive State

模式 2:使用 Signals 实现响应式状态

Use
signal()
for writable state,
computed()
for derived values, and
effect()
for side effects. Key rules: always use
.set()
or
.update()
for mutations (never mutate the value directly), use
computed()
for derived values (not methods), and reserve
effect()
for true side effects (logging, analytics, localStorage).
typescript
// Writable signal
count = signal(0);

// Computed signal (read-only, memoized, recalculates only when deps change)
doubleCount = computed(() => this.count() * 2);

// Updating signals - always immutable
this.count.set(5); // Replace value
this.count.update((value) => value + 1); // Update from previous

// For arrays/objects: return new references
items = signal<Item[]>([]);
this.items.update((items) => [...items, newItem]); // Spread, don't push

// Effect for side effects only (not derived state)
effect(() => console.log(`Count: ${this.count()}`));
See examples/core.md for a full shopping cart example with signals.
typescript
// BAD - Direct mutation doesn't trigger reactivity
this.items().push(newItem);              // signal won't notify consumers
this.items.update(items => { items.push(newItem); return items; }); // same reference, no update

// BAD - Method instead of computed (recalculates every call, not memoized)
getTotal(): number { return this.items().reduce(...); }
Why bad: direct mutation doesn't trigger change detection, returning same reference skips equality check, methods lack memoization that computed() provides

使用
signal()
定义可写状态,
computed()
定义派生值,
effect()
处理副作用。核心规则:始终使用
.set()
.update()
进行变更(绝不能直接修改值),使用
computed()
处理派生值(而非方法),仅将
effect()
用于真正的副作用(日志、分析、localStorage)。
typescript
// Writable signal
count = signal(0);

// Computed signal (read-only, memoized, recalculates only when deps change)
doubleCount = computed(() => this.count() * 2);

// Updating signals - always immutable
this.count.set(5); // Replace value
this.count.update((value) => value + 1); // Update from previous

// For arrays/objects: return new references
items = signal<Item[]>([]);
this.items.update((items) => [...items, newItem]); // Spread, don't push

// Effect for side effects only (not derived state)
effect(() => console.log(`Count: ${this.count()}`));
完整的购物车示例请查看 examples/core.md
typescript
// BAD - Direct mutation doesn't trigger reactivity
this.items().push(newItem);              // signal won't notify consumers
this.items.update(items => { items.push(newItem); return items; }); // same reference, no update

// BAD - Method instead of computed (recalculates every call, not memoized)
getTotal(): number { return this.items().reduce(...); }
劣势: 直接修改不会触发变更检测,返回相同引用会跳过相等性检查,方法缺乏 computed() 提供的缓存能力

Pattern 3: Signal Inputs and Outputs

模式 3:基于 Signal 的输入和输出

Use
input()
,
output()
, and
model()
functions for component communication.
typescript
// search-input.component.ts
import { Component, input, output, model, computed } from "@angular/core";

const MIN_SEARCH_LENGTH = 3;

@Component({
  selector: "app-search-input",
  standalone: true,
  template: `
    <div class="search-input">
      <input
        [value]="query()"
        (input)="onInput($event)"
        [placeholder]="placeholder()"
      />
      @if (isValidSearch()) {
        <button (click)="search.emit(query())">Search</button>
      }
      @if (query()) {
        <button (click)="clear()">Clear</button>
      }
    </div>
  `,
})
export class SearchInputComponent {
  // Optional input with default value
  placeholder = input("Search...");

  // Required input
  minLength = input.required<number>();

  // Two-way binding with model()
  query = model("");

  // Output event
  search = output<string>();

  // Computed from inputs
  isValidSearch = computed(() => this.query().length >= this.minLength());

  onInput(event: Event): void {
    const target = event.target as HTMLInputElement;
    this.query.set(target.value);
  }

  clear(): void {
    this.query.set("");
  }
}
Usage in parent:
html
<app-search-input
  [minLength]="3"
  [(query)]="searchQuery"
  (search)="onSearch($event)"
/>
Why good: input() and input.required() clearly distinguish optional vs required props, model() enables two-way binding with [(query)] syntax, computed() derives validation state reactively, output() provides type-safe event emission

使用
input()
output()
model()
函数实现组件通信。
typescript
// search-input.component.ts
import { Component, input, output, model, computed } from "@angular/core";

const MIN_SEARCH_LENGTH = 3;

@Component({
  selector: "app-search-input",
  standalone: true,
  template: `
    <div class="search-input">
      <input
        [value]="query()"
        (input)="onInput($event)"
        [placeholder]="placeholder()"
      />
      @if (isValidSearch()) {
        <button (click)="search.emit(query())">Search</button>
      }
      @if (query()) {
        <button (click)="clear()">Clear</button>
      }
    </div>
  `,
})
export class SearchInputComponent {
  // Optional input with default value
  placeholder = input("Search...");

  // Required input
  minLength = input.required<number>();

  // Two-way binding with model()
  query = model("");

  // Output event
  search = output<string>();

  // Computed from inputs
  isValidSearch = computed(() => this.query().length >= this.minLength());

  onInput(event: Event): void {
    const target = event.target as HTMLInputElement;
    this.query.set(target.value);
  }

  clear(): void {
    this.query.set("");
  }
}
父组件中使用:
html
<app-search-input
  [minLength]="3"
  [(query)]="searchQuery"
  (search)="onSearch($event)"
/>
优势: input() 和 input.required() 清晰区分可选与必填属性,model() 支持 [(query)] 语法实现双向绑定,computed() 响应式派生验证状态,output() 提供类型安全的事件触发

Pattern 4: Control Flow with @if, @for, @switch

模式 4:使用 @if、@for、@switch 实现控制流

Use built-in control flow blocks instead of structural directives.
typescript
// user-list.component.ts
import { Component, input, output } from "@angular/core";
import type { User } from "./user.types";

type LoadingState = "idle" | "loading" | "error" | "success";

@Component({
  selector: "app-user-list",
  standalone: true,
  template: `
    @switch (state()) {
      @case ("loading") {
        <div class="loading">Loading users...</div>
      }
      @case ("error") {
        <div class="error">
          <p>Failed to load users</p>
          <button (click)="retry.emit()">Retry</button>
        </div>
      }
      @case ("success") {
        @if (users().length > 0) {
          <ul class="user-list">
            @for (
              user of users();
              track user.id;
              let i = $index, first = $first, last = $last
            ) {
              <li [class.first]="first" [class.last]="last">
                <span class="index">{{ i + 1 }}.</span>
                <span class="name">{{ user.name }}</span>
                <span class="email">{{ user.email }}</span>
              </li>
            } @empty {
              <li class="empty">No users found</li>
            }
          </ul>
        } @else {
          <p>No users available</p>
        }
      }
      @default {
        <p>Ready to load users</p>
      }
    }
  `,
})
export class UserListComponent {
  users = input.required<User[]>();
  state = input<LoadingState>("idle");
  retry = output<void>();
}
Why good: @switch provides clear multi-branch logic, @for with track enables efficient DOM updates, @empty handles empty collections elegantly, $index/$first/$last provide iteration context without extra code, no CommonModule import required
typescript
// BAD - Legacy structural directives
@Component({
  imports: [CommonModule], // Extra import needed
  template: `
    <div *ngIf="loading; else content">Loading...</div>
    <ng-template #content>
      <ul>
        <li *ngFor="let user of users; trackBy: trackByFn; let i = index">
          {{ user.name }}
        </li>
      </ul>
    </ng-template>
  `,
})
export class UserListComponent {
  trackByFn(index: number, user: User): string {
    return user.id; // Separate function needed
  }
}
Why bad: requires CommonModule import, trackBy requires separate function, ng-template syntax is verbose, less optimal type narrowing

使用内置控制流块替代结构型指令。
typescript
// user-list.component.ts
import { Component, input, output } from "@angular/core";
import type { User } from "./user.types";

type LoadingState = "idle" | "loading" | "error" | "success";

@Component({
  selector: "app-user-list",
  standalone: true,
  template: `
    @switch (state()) {
      @case ("loading") {
        <div class="loading">Loading users...</div>
      }
      @case ("error") {
        <div class="error">
          <p>Failed to load users</p>
          <button (click)="retry.emit()">Retry</button>
        </div>
      }
      @case ("success") {
        @if (users().length > 0) {
          <ul class="user-list">
            @for (
              user of users();
              track user.id;
              let i = $index, first = $first, last = $last
            ) {
              <li [class.first]="first" [class.last]="last">
                <span class="index">{{ i + 1 }}.</span>
                <span class="name">{{ user.name }}</span>
                <span class="email">{{ user.email }}</span>
              </li>
            } @empty {
              <li class="empty">No users found</li>
            }
          </ul>
        } @else {
          <p>No users available</p>
        }
      }
      @default {
        <p>Ready to load users</p>
      }
    }
  `,
})
export class UserListComponent {
  users = input.required<User[]>();
  state = input<LoadingState>("idle");
  retry = output<void>();
}
优势: @switch 提供清晰的多分支逻辑,带 track 的 @for 实现高效 DOM 更新,@empty 优雅处理空集合,$index/$first/$last 无需额外代码提供迭代上下文,无需导入 CommonModule
typescript
// BAD - Legacy structural directives
@Component({
  imports: [CommonModule], // Extra import needed
  template: `
    <div *ngIf="loading; else content">Loading...</div>
    <ng-template #content>
      <ul>
        <li *ngFor="let user of users; trackBy: trackByFn; let i = index">
          {{ user.name }}
        </li>
      </ul>
    </ng-template>
  `,
})
export class UserListComponent {
  trackByFn(index: number, user: User): string {
    return user.id; // Separate function needed
  }
}
劣势: 需要导入 CommonModule,trackBy 需要单独函数,ng-template 语法冗长,类型收窄效果不佳

Pattern 5: Deferred Loading with @defer

模式 5:使用 @defer 实现延迟加载

Use
@defer
for lazy loading components and improving initial bundle size.
typescript
// dashboard.component.ts
import { Component, signal } from "@angular/core";

@Component({
  selector: "app-dashboard",
  standalone: true,
  template: `
    <h1>Dashboard</h1>

    <!-- Defer loading until viewport -->
    @defer (on viewport) {
      <app-heavy-chart />
    } @placeholder (minimum 200ms) {
      <div class="chart-skeleton">Chart loading...</div>
    } @loading (after 100ms; minimum 500ms) {
      <div class="spinner">Loading chart...</div>
    } @error {
      <div class="error">Failed to load chart</div>
    }

    <!-- Defer loading on interaction -->
    @defer (on interaction) {
      <app-comments-section />
    } @placeholder {
      <button>Load Comments</button>
    }

    <!-- Defer with condition -->
    @defer (when showAdvanced()) {
      <app-advanced-settings />
    } @placeholder {
      <p>Advanced settings will load when enabled</p>
    }

    <!-- Prefetch for faster navigation -->
    @defer (on idle; prefetch on hover) {
      <app-related-items />
    } @placeholder {
      <div class="related-skeleton">Related items</div>
    }
  `,
})
export class DashboardComponent {
  showAdvanced = signal(false);
}
Why good: @defer reduces initial bundle size by lazy-loading components, @placeholder prevents layout shift during load, @loading shows progress after delay to avoid flicker, @error handles failures gracefully, prefetch optimizes perceived performance
When to use @defer:
  • Heavy components below the fold (charts, data tables)
  • Features triggered by user interaction (comments, modals)
  • Conditional features that may never be needed
  • Components that can be prefetched on idle/hover
When NOT to use @defer:
  • Components visible on initial load (above the fold)
  • Critical UI that users need immediately
  • Components that would cause layout shift when loaded

使用
@defer
懒加载组件,优化初始打包体积。
typescript
// dashboard.component.ts
import { Component, signal } from "@angular/core";

@Component({
  selector: "app-dashboard",
  standalone: true,
  template: `
    <h1>Dashboard</h1>

    <!-- Defer loading until viewport -->
    @defer (on viewport) {
      <app-heavy-chart />
    } @placeholder (minimum 200ms) {
      <div class="chart-skeleton">Chart loading...</div>
    } @loading (after 100ms; minimum 500ms) {
      <div class="spinner">Loading chart...</div>
    } @error {
      <div class="error">Failed to load chart</div>
    }

    <!-- Defer loading on interaction -->
    @defer (on interaction) {
      <app-comments-section />
    } @placeholder {
      <button>Load Comments</button>
    }

    <!-- Defer with condition -->
    @defer (when showAdvanced()) {
      <app-advanced-settings />
    } @placeholder {
      <p>Advanced settings will load when enabled</p>
    }

    <!-- Prefetch for faster navigation -->
    @defer (on idle; prefetch on hover) {
      <app-related-items />
    } @placeholder {
      <div class="related-skeleton">Related items</div>
    }
  `,
})
export class DashboardComponent {
  showAdvanced = signal(false);
}
优势: @defer 通过懒加载组件减少初始打包体积,@placeholder 避免加载时布局偏移,@loading 延迟显示进度避免闪烁,@error 优雅处理加载失败,预取优化感知性能
@defer 适用场景:
  • 视口下方的重型组件(图表、数据表格)
  • 用户交互触发的功能(评论、模态框)
  • 可能永远不会用到的条件性功能
  • 可在空闲/悬停时预取的组件
@defer 不适用场景:
  • 初始加载时可见的组件(视口上方)
  • 用户立即需要的关键 UI
  • 加载时会导致布局偏移的组件

Pattern 6: Dependency Injection with inject()

模式 6:使用 inject() 实现依赖注入

Use
inject()
function instead of constructor injection for cleaner, more flexible DI.
typescript
// user.service.ts
import { Injectable, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import type { User } from "./user.types";

const API_BASE_URL = "/api";

@Injectable({ providedIn: "root" })
export class UserService {
  private http = inject(HttpClient);

  getUsers() {
    return this.http.get<User[]>(`${API_BASE_URL}/users`);
  }

  getUser(id: string) {
    return this.http.get<User>(`${API_BASE_URL}/users/${id}`);
  }
}
typescript
// user-profile.component.ts
import { Component, inject, resource } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { toSignal } from "@angular/core/rxjs-interop";
import type { User } from "./user.types";

const API_BASE_URL = "/api";

@Component({
  selector: "app-user-profile",
  standalone: true,
  template: `
    @if (userResource.isLoading()) {
      <p>Loading user...</p>
    }
    @if (userResource.hasValue()) {
      <h1>{{ userResource.value().name }}</h1>
      <p>{{ userResource.value().email }}</p>
    }
    @if (userResource.error(); as error) {
      <p>Error: {{ error }}</p>
      <button (click)="userResource.reload()">Retry</button>
    }
  `,
})
export class UserProfileComponent {
  private route = inject(ActivatedRoute);

  // Convert route params to signal
  private params = toSignal(this.route.params, { initialValue: { id: "" } });

  // resource() auto-refetches when userId changes
  userResource = resource({
    params: () => ({ id: this.params()["id"] }),
    loader: async ({ params, abortSignal }) => {
      const response = await fetch(`${API_BASE_URL}/users/${params.id}`, {
        signal: abortSignal,
      });
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return response.json() as Promise<User>;
    },
  });
}
Why good: inject() provides cleaner syntax without constructor boilerplate, resource() handles loading/error states and race conditions automatically, no manual signal + effect combo needed
typescript
// BAD - Constructor injection (legacy)
export class UserProfileComponent {
  constructor(
    private route: ActivatedRoute,
    private userService: UserService,
  ) {}
}
Why bad: constructor injection requires boilerplate, doesn't work in field initializers, less flexible for conditional injection
inject() with options:
typescript
// Optional injection
private optionalService = inject(OptionalService, { optional: true });

// Skip self (look in parent injectors)
private parentService = inject(ParentService, { skipSelf: true });

// Self only (don't look in parent injectors)
private selfService = inject(SelfService, { self: true });

使用
inject()
函数替代构造函数注入,实现更简洁、灵活的 DI。
typescript
// user.service.ts
import { Injectable, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import type { User } from "./user.types";

const API_BASE_URL = "/api";

@Injectable({ providedIn: "root" })
export class UserService {
  private http = inject(HttpClient);

  getUsers() {
    return this.http.get<User[]>(`${API_BASE_URL}/users`);
  }

  getUser(id: string) {
    return this.http.get<User>(`${API_BASE_URL}/users/${id}`);
  }
}
typescript
// user-profile.component.ts
import { Component, inject, resource } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { toSignal } from "@angular/core/rxjs-interop";
import type { User } from "./user.types";

const API_BASE_URL = "/api";

@Component({
  selector: "app-user-profile",
  standalone: true,
  template: `
    @if (userResource.isLoading()) {
      <p>Loading user...</p>
    }
    @if (userResource.hasValue()) {
      <h1>{{ userResource.value().name }}</h1>
      <p>{{ userResource.value().email }}</p>
    }
    @if (userResource.error(); as error) {
      <p>Error: {{ error }}</p>
      <button (click)="userResource.reload()">Retry</button>
    }
  `,
})
export class UserProfileComponent {
  private route = inject(ActivatedRoute);

  // Convert route params to signal
  private params = toSignal(this.route.params, { initialValue: { id: "" } });

  // resource() auto-refetches when userId changes
  userResource = resource({
    params: () => ({ id: this.params()["id"] }),
    loader: async ({ params, abortSignal }) => {
      const response = await fetch(`${API_BASE_URL}/users/${params.id}`, {
        signal: abortSignal,
      });
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return response.json() as Promise<User>;
    },
  });
}
优势: inject() 提供更简洁的语法,无需构造函数冗余代码,resource() 自动处理加载/错误状态和竞态条件,无需手动组合 signal + effect
typescript
// BAD - Constructor injection (legacy)
export class UserProfileComponent {
  constructor(
    private route: ActivatedRoute,
    private userService: UserService,
  ) {}
}
劣势: 构造函数注入需要冗余代码,无法在字段初始化器中使用,条件注入灵活性更低
带选项的 inject():
typescript
// Optional injection
private optionalService = inject(OptionalService, { optional: true });

// Skip self (look in parent injectors)
private parentService = inject(ParentService, { skipSelf: true });

// Self only (don't look in parent injectors)
private selfService = inject(SelfService, { self: true });

Pattern 7: Routing with Standalone Components

模式 7:独立组件路由配置

Configure routing using
provideRouter
and lazy load with
loadComponent
.
typescript
// app.config.ts
import { ApplicationConfig } from "@angular/core";
import {
  provideRouter,
  withComponentInputBinding,
  withPreloading,
  PreloadAllModules,
} from "@angular/router";
import { provideHttpClient } from "@angular/common/http";
import { routes } from "./app.routes";

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withComponentInputBinding(), // Bind route params to inputs
      withPreloading(PreloadAllModules), // Preload lazy routes
    ),
    provideHttpClient(),
  ],
};
typescript
// app.routes.ts
import type { Routes } from "@angular/router";

export const routes: Routes = [
  {
    path: "",
    loadComponent: () =>
      import("./home/home.component").then((m) => m.HomeComponent),
  },
  {
    path: "users",
    loadComponent: () =>
      import("./users/user-list.component").then((m) => m.UserListComponent),
  },
  {
    path: "users/:id",
    loadComponent: () =>
      import("./users/user-detail.component").then(
        (m) => m.UserDetailComponent,
      ),
  },
  {
    path: "admin",
    loadComponent: () =>
      import("./admin/admin.component").then((m) => m.AdminComponent),
    canActivate: [authGuard],
  },
  {
    path: "**",
    loadComponent: () =>
      import("./not-found/not-found.component").then(
        (m) => m.NotFoundComponent,
      ),
  },
];
typescript
// user-detail.component.ts - Using withComponentInputBinding
import { Component, input } from "@angular/core";

@Component({
  selector: "app-user-detail",
  standalone: true,
  template: `
    <h1>User {{ id() }}</h1>
    @if (tab()) {
      <p>Active tab: {{ tab() }}</p>
    }
  `,
})
export class UserDetailComponent {
  // Route param :id bound automatically with withComponentInputBinding
  id = input.required<string>();

  // Query param ?tab bound automatically
  tab = input<string | undefined>();
}
Why good: provideRouter replaces RouterModule.forRoot(), loadComponent lazy loads individual components without wrapper modules, withComponentInputBinding eliminates ActivatedRoute boilerplate, preloading improves navigation performance

使用
provideRouter
配置路由,使用
loadComponent
实现懒加载。
typescript
// app.config.ts
import { ApplicationConfig } from "@angular/core";
import {
  provideRouter,
  withComponentInputBinding,
  withPreloading,
  PreloadAllModules,
} from "@angular/router";
import { provideHttpClient } from "@angular/common/http";
import { routes } from "./app.routes";

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withComponentInputBinding(), // Bind route params to inputs
      withPreloading(PreloadAllModules), // Preload lazy routes
    ),
    provideHttpClient(),
  ],
};
typescript
// app.routes.ts
import type { Routes } from "@angular/router";

export const routes: Routes = [
  {
    path: "",
    loadComponent: () =>
      import("./home/home.component").then((m) => m.HomeComponent),
  },
  {
    path: "users",
    loadComponent: () =>
      import("./users/user-list.component").then((m) => m.UserListComponent),
  },
  {
    path: "users/:id",
    loadComponent: () =>
      import("./users/user-detail.component").then(
        (m) => m.UserDetailComponent,
      ),
  },
  {
    path: "admin",
    loadComponent: () =>
      import("./admin/admin.component").then((m) => m.AdminComponent),
    canActivate: [authGuard],
  },
  {
    path: "**",
    loadComponent: () =>
      import("./not-found/not-found.component").then(
        (m) => m.NotFoundComponent,
      ),
  },
];
typescript
// user-detail.component.ts - Using withComponentInputBinding
import { Component, input } from "@angular/core";

@Component({
  selector: "app-user-detail",
  standalone: true,
  template: `
    <h1>User {{ id() }}</h1>
    @if (tab()) {
      <p>Active tab: {{ tab() }}</p>
    }
  `,
})
export class UserDetailComponent {
  // Route param :id bound automatically with withComponentInputBinding
  id = input.required<string>();

  // Query param ?tab bound automatically
  tab = input<string | undefined>();
}
优势: provideRouter 替代 RouterModule.forRoot(),loadComponent 无需包装模块即可懒加载单个组件,withComponentInputBinding 消除 ActivatedRoute 冗余代码,预加载优化导航性能

Pattern 8: Lifecycle Hooks with Signals

模式 8:基于 Signals 的生命周期钩子

Replace traditional lifecycle hooks with signal-based patterns.
typescript
// resize-observer.component.ts
import {
  Component,
  ElementRef,
  signal,
  inject,
  afterNextRender,
  afterRender,
  DestroyRef,
} from "@angular/core";

const DEBOUNCE_MS = 100;

@Component({
  selector: "app-resize-observer",
  standalone: true,
  template: `
    <div #container class="container">
      <p>Width: {{ width() }}px</p>
      <p>Height: {{ height() }}px</p>
    </div>
  `,
})
export class ResizeObserverComponent {
  private elementRef = inject(ElementRef);
  private destroyRef = inject(DestroyRef);

  width = signal(0);
  height = signal(0);

  constructor() {
    // Run once after first render (replaces ngAfterViewInit for DOM setup)
    afterNextRender(() => {
      this.setupResizeObserver();
    });

    // Run after every render (use sparingly)
    afterRender(() => {
      console.log("Component rendered");
    });
  }

  private setupResizeObserver(): void {
    const element = this.elementRef.nativeElement;
    const observer = new ResizeObserver((entries) => {
      for (const entry of entries) {
        this.width.set(entry.contentRect.width);
        this.height.set(entry.contentRect.height);
      }
    });

    observer.observe(element);

    // Cleanup on destroy (replaces ngOnDestroy)
    this.destroyRef.onDestroy(() => {
      observer.disconnect();
    });
  }
}
Why good: afterNextRender runs after first render for DOM setup, afterRender provides per-render hooks, DestroyRef.onDestroy handles cleanup without implementing OnDestroy, signals automatically trigger change detection
Lifecycle hook mapping:
Legacy HookSignal-Based Alternative
ngOnInitconstructor + effect()
ngOnChangeseffect() watching input() signals
ngAfterViewInitafterNextRender()
ngAfterViewCheckedafterRender() (afterEveryRender() in v20+)
ngOnDestroyDestroyRef.onDestroy()
DOM side effectsafterRenderEffect() with phases (v19+)
</patterns>
<integration>
使用基于 Signal 的模式替代传统生命周期钩子。
typescript
// resize-observer.component.ts
import {
  Component,
  ElementRef,
  signal,
  inject,
  afterNextRender,
  afterRender,
  DestroyRef,
} from "@angular/core";

const DEBOUNCE_MS = 100;

@Component({
  selector: "app-resize-observer",
  standalone: true,
  template: `
    <div #container class="container">
      <p>Width: {{ width() }}px</p>
      <p>Height: {{ height() }}px</p>
    </div>
  `,
})
export class ResizeObserverComponent {
  private elementRef = inject(ElementRef);
  private destroyRef = inject(DestroyRef);

  width = signal(0);
  height = signal(0);

  constructor() {
    // Run once after first render (replaces ngAfterViewInit for DOM setup)
    afterNextRender(() => {
      this.setupResizeObserver();
    });

    // Run after every render (use sparingly)
    afterRender(() => {
      console.log("Component rendered");
    });
  }

  private setupResizeObserver(): void {
    const element = this.elementRef.nativeElement;
    const observer = new ResizeObserver((entries) => {
      for (const entry of entries) {
        this.width.set(entry.contentRect.width);
        this.height.set(entry.contentRect.height);
      }
    });

    observer.observe(element);

    // Cleanup on destroy (replaces ngOnDestroy)
    this.destroyRef.onDestroy(() => {
      observer.disconnect();
    });
  }
}
优势: afterNextRender 在首次渲染后执行,用于 DOM 初始化,afterRender 提供每次渲染后的钩子,DestroyRef.onDestroy 无需实现 OnDestroy 即可处理清理,signals 自动触发变更检测
生命周期钩子映射:
遗留钩子基于 Signal 的替代方案
ngOnInitconstructor + effect()
ngOnChangeseffect() 监听 input() signals
ngAfterViewInitafterNextRender()
ngAfterViewCheckedafterRender() (v20+ 中为 afterEveryRender())
ngOnDestroyDestroyRef.onDestroy()
DOM 副作用afterRenderEffect() 配合阶段参数 (v19+)
</patterns>
<integration>

Integration Guide

集成指南

Angular standalone architecture is self-contained. Components declare their own imports and providers. Routing uses
provideRouter
. Services use
providedIn: "root"
or component-level providers.
Bootstrapping:
typescript
// main.ts
import { bootstrapApplication } from "@angular/platform-browser";
import { AppComponent } from "./app/app.component";
import { appConfig } from "./app/app.config";

bootstrapApplication(AppComponent, appConfig).catch((err) =>
  console.error(err),
);
Component Communication:
  • Parent to child:
    input()
    and
    input.required()
  • Child to parent:
    output()
    with
    .emit()
  • Two-way binding:
    model()
    with
    [()]
    syntax
  • Across tree: Services with
    inject()
RxJS Interop:
typescript
import { toSignal, toObservable } from "@angular/core/rxjs-interop";

// Observable to Signal
const users = toSignal(this.userService.getUsers(), { initialValue: [] });

// Signal to Observable
const count$ = toObservable(this.count);
</integration>
<red_flags>
Angular 独立架构是自包含的。 组件自行声明导入项和提供者。路由使用
provideRouter
。服务使用
providedIn: "root"
或组件级提供者。
启动应用:
typescript
// main.ts
import { bootstrapApplication } from "@angular/platform-browser";
import { AppComponent } from "./app/app.component";
import { appConfig } from "./app/app.config";

bootstrapApplication(AppComponent, appConfig).catch((err) =>
  console.error(err),
);
组件通信:
  • 父到子:
    input()
    input.required()
  • 子到父:
    output()
    配合
    .emit()
  • 双向绑定:
    model()
    配合
    [()]
    语法
  • 跨组件树:使用
    inject()
    注入服务
RxJS 互操作:
typescript
import { toSignal, toObservable } from "@angular/core/rxjs-interop";

// Observable 转 Signal
const users = toSignal(this.userService.getUsers(), { initialValue: [] });

// Signal 转 Observable
const count$ = toObservable(this.count);
</integration>
<red_flags>

RED FLAGS

注意事项

High Priority:
  • Using @Input/@Output decorators - Legacy pattern; use
    input()
    ,
    output()
    ,
    model()
    signal functions
  • **Using ngIf/ngFor/*ngSwitch - Legacy directives; use
    @if
    ,
    @for
    ,
    @switch
    built-in control flow
  • Missing
    track
    in @for
    - Causes unnecessary DOM recreation and poor performance
  • Constructor injection instead of inject() - More boilerplate, less flexible
  • Mutating signal values directly -
    signal().push(item)
    doesn't trigger updates; use
    .update()
    with spread
  • Manual signal sync instead of linkedSignal() - Use
    linkedSignal()
    for writable derived state (v19+)
  • Using resource() for mutations -
    resource()
    /
    rxResource()
    /
    httpResource()
    are read-only; use HttpClient for POST/PUT/DELETE
Medium Priority:
  • @defer above the fold - Hurts LCP and CLS Core Web Vitals
  • effect() for derived state - Use
    computed()
    or
    linkedSignal()
    instead
  • effect() for DOM operations - Use
    afterRenderEffect()
    with phases
  • toSignal() without initialValue - Can cause runtime errors if observable hasn't emitted
  • Not checking resource hasValue() - Use
    hasValue()
    as type guard before accessing
    value()
Gotchas & Edge Cases:
  • signal()
    uses
    Object.is()
    equality by default; provide custom equality for objects
  • inject()
    must be called in constructor or field initializer, not in methods
  • @defer
    always renders
    @placeholder
    on server (SSR); triggers are ignored server-side
  • linkedSignal()
    value resets when source signal changes; use computation form to preserve previous
  • afterRenderEffect()
    without phase specification defaults to
    mixedReadWrite
    which can cause layout thrashing
See reference.md for complete decision frameworks, anti-patterns with code examples, and quick reference tables.
</red_flags>

<critical_reminders>
高优先级:
  • 使用 @Input/@Output 装饰器 - 遗留模式;使用
    input()
    output()
    model()
    信号函数
  • **使用 *ngIf/ngFor/ngSwitch - 遗留指令;使用
    @if
    @for
    @switch
    内置控制流
  • @for 中缺少
    track
    - 导致不必要的 DOM 重建和性能问题
  • 使用构造函数注入而非 inject() - 冗余代码更多,灵活性更低
  • 直接修改 signal 值 -
    signal().push(item)
    不会触发更新;使用
    .update()
    配合扩展运算符
  • 手动同步信号而非使用 linkedSignal() - 对于依赖型可写状态,使用
    linkedSignal()
    (v19+)
  • 使用 resource() 进行变更操作 -
    resource()
    /
    rxResource()
    /
    httpResource()
    是只读的;使用 HttpClient 处理 POST/PUT/DELETE
中优先级:
  • @defer 用于视口上方组件 - 损害 LCP 和 CLS 核心 Web 指标
  • 使用 effect() 处理派生状态 - 改用
    computed()
    linkedSignal()
  • 使用 effect() 处理 DOM 操作 - 使用带阶段参数的
    afterRenderEffect()
  • toSignal() 未指定 initialValue - 若 Observable 未发射值可能导致运行时错误
  • 未检查 resource 的 hasValue() - 在访问
    value()
    前使用
    hasValue()
    作为类型守卫
陷阱与边缘情况:
  • signal()
    默认使用
    Object.is()
    进行相等性检查;对于对象需提供自定义相等性函数
  • inject()
    必须在构造函数或字段初始化器中调用,不能在方法中调用
  • @defer
    在服务器端(SSR)始终渲染
    @placeholder
    ;触发条件在服务器端被忽略
  • linkedSignal()
    值会随源信号变化而重置;使用计算形式保留之前的值
  • afterRenderEffect()
    未指定阶段时默认使用
    mixedReadWrite
    ,可能导致布局抖动
完整的决策框架、带代码示例的反模式及快速参考表请查看 reference.md
</red_flags>

<critical_reminders>

CRITICAL REMINDERS

重要提醒

All code must follow project conventions in CLAUDE.md
(You MUST write standalone components (the default in Angular 19) - only specify
standalone: false
when intentionally using NgModules)
(You MUST use
input()
,
output()
,
model()
functions instead of
@Input()
,
@Output()
decorators)
(You MUST use
inject()
function for dependency injection, NOT constructor injection)
(You MUST use
@if
,
@for
,
@switch
control flow blocks, NOT
*ngIf
,
*ngFor
,
*ngSwitch
)
(You MUST use
track
expression in ALL
@for
loops)
(You MUST use
linkedSignal()
instead of manual signal synchronization for dependent writable state)
Failure to follow these rules will produce legacy Angular code that misses performance optimizations and modern reactivity benefits.
</critical_reminders>
所有代码必须遵循 CLAUDE.md 中的项目规范
(必须编写独立组件——Angular 19 默认模式——仅在有意使用 NgModules 时才指定
standalone: false
(必须使用
input()
output()
model()
函数,而非
@Input()
@Output()
装饰器)
(必须使用
inject()
函数进行依赖注入,而非构造函数注入)
(必须使用
@if
@for
@switch
控制流块,而非
*ngIf
*ngFor
*ngSwitch
(所有
@for
循环必须使用
track
表达式)
(对于依赖型可写状态,必须使用
linkedSignal()
而非手动同步信号)
不遵循这些规则将生成遗留 Angular 代码,无法获得性能优化和现代响应式能力的优势。
</critical_reminders>