web-framework-angular-standalone
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAngular Standalone Components
Angular 独立组件
Quick Guide: Components are standalone by default in Angular 19. Use,signal(),computed(),effect()for reactive state. UselinkedSignal(),input(),output()for component communication. Usemodel(),@if,@for,@switchfor template control flow. Use@deferfor dependency injection. Useinject()for async data fetching.resource()
<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,, named constants)import type
(You MUST write standalone components (the default in Angular 19) - only specify when intentionally using NgModules)
standalone: false(You MUST use , , functions instead of , decorators)
input()output()model()@Input()@Output()(You MUST use function for dependency injection, NOT constructor injection)
inject()(You MUST use , , control flow blocks, NOT , , )
@if@for@switch*ngIf*ngFor*ngSwitch(You MUST use expression in ALL loops)
track@for(You MUST use instead of manual signal synchronization for dependent writable state)
linkedSignal()</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 or
@deferloadComponent - Fetching async data with ,
resource(), orrxResource()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(所有 循环必须使用 表达式)
@fortrack(对于依赖型可写状态,必须使用 而非手动同步信号)
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, is the default - you only need to specify for NgModule components. Signals provide synchronous, fine-grained reactivity for predictable state management. The new control flow syntax (, , , ) 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.
standalone: truestandalone: false@if@for@switch@deferAngular's Four Pillars (17-19):
- Standalone by Default - Components, directives, and pipes are standalone by default in v19
- Signal-Based Reactivity - Synchronous, memoized, fine-grained change detection with ,
signal(),computed()linkedSignal() - Built-In Control Flow - Template syntax that requires no imports and optimizes at build time
- Resource API - Experimental async data fetching that integrates with signals (,
resource(),rxResource()in 19.2)httpResource()
<patterns>
Angular 17-19 采用独立优先架构,消除了 NgModule 的冗余代码。在 Angular 19 中, 是默认配置——仅在使用 NgModule 组件时才需要指定 。Signals 提供同步、细粒度的响应式能力,实现可预测的状态管理。新的控制流语法(、、、)内置在模板中无需导入,提供更优的类型收窄和更小的打包体积。组件应是自包含、可懒加载的单元,自行声明依赖项。
standalone: truestandalone: false@if@for@switch@deferAngular 17-19 的四大支柱:
- 默认独立 - v19 中组件、指令、管道默认是独立的
- 基于 Signal 的响应式 - 使用 、
signal()、computed()实现同步、缓存、细粒度变更检测linkedSignal() - 内置控制流 - 模板语法无需导入,构建时自动优化
- Resource API - 实验性异步数据获取方案,与 signals 集成(19.2 版本中的 、
resource()、rxResource())httpResource()
<patterns>
Core Patterns
核心模式
Pattern 1: Standalone Component Structure
模式 1:独立组件结构
All Angular 17-19 components use (the default in Angular 19) and declare their own imports.
standalone: truetypescript
// 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 组件使用 (Angular 19 默认)并自行声明导入项。
standalone: truetypescript
// 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 for writable state, for derived values, and for side effects. Key rules: always use or for mutations (never mutate the value directly), use for derived values (not methods), and reserve for true side effects (logging, analytics, localStorage).
signal()computed()effect().set().update()computed()effect()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
使用 定义可写状态, 定义派生值, 处理副作用。核心规则:始终使用 或 进行变更(绝不能直接修改值),使用 处理派生值(而非方法),仅将 用于真正的副作用(日志、分析、localStorage)。
signal()computed()effect().set().update()computed()effect()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 , , and functions for component communication.
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("");
}
}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 for lazy loading components and improving initial bundle size.
@defertypescript
// 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
使用 懒加载组件,优化初始打包体积。
@defertypescript
// 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 function instead of constructor injection for cleaner, more flexible DI.
inject()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 });使用 函数替代构造函数注入,实现更简洁、灵活的 DI。
inject()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 and lazy load with .
provideRouterloadComponenttypescript
// 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
使用 配置路由,使用 实现懒加载。
provideRouterloadComponenttypescript
// 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 Hook | Signal-Based Alternative |
|---|---|
| ngOnInit | constructor + effect() |
| ngOnChanges | effect() watching input() signals |
| ngAfterViewInit | afterNextRender() |
| ngAfterViewChecked | afterRender() (afterEveryRender() in v20+) |
| ngOnDestroy | DestroyRef.onDestroy() |
| DOM side effects | afterRenderEffect() with phases (v19+) |
<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 的替代方案 |
|---|---|
| ngOnInit | constructor + effect() |
| ngOnChanges | effect() 监听 input() signals |
| ngAfterViewInit | afterNextRender() |
| ngAfterViewChecked | afterRender() (v20+ 中为 afterEveryRender()) |
| ngOnDestroy | DestroyRef.onDestroy() |
| DOM 副作用 | afterRenderEffect() 配合阶段参数 (v19+) |
<integration>
Integration Guide
集成指南
Angular standalone architecture is self-contained. Components declare their own imports and providers. Routing uses . Services use or component-level providers.
provideRouterprovidedIn: "root"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: and
input()input.required() - Child to parent: with
output().emit() - Two-way binding: with
model()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);<red_flags>
Angular 独立架构是自包含的。 组件自行声明导入项和提供者。路由使用 。服务使用 或组件级提供者。
provideRouterprovidedIn: "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);<red_flags>
RED FLAGS
注意事项
High Priority:
- Using @Input/@Output decorators - Legacy pattern; use ,
input(),output()signal functionsmodel() - **Using ngIf/ngFor/*ngSwitch - Legacy directives; use ,
@if,@forbuilt-in control flow@switch - Missing in @for - Causes unnecessary DOM recreation and poor performance
track - Constructor injection instead of inject() - More boilerplate, less flexible
- Mutating signal values directly - doesn't trigger updates; use
signal().push(item)with spread.update() - Manual signal sync instead of linkedSignal() - Use for writable derived state (v19+)
linkedSignal() - Using resource() for mutations - /
resource()/rxResource()are read-only; use HttpClient for POST/PUT/DELETEhttpResource()
Medium Priority:
- @defer above the fold - Hurts LCP and CLS Core Web Vitals
- effect() for derived state - Use or
computed()insteadlinkedSignal() - effect() for DOM operations - Use with phases
afterRenderEffect() - toSignal() without initialValue - Can cause runtime errors if observable hasn't emitted
- Not checking resource hasValue() - Use as type guard before accessing
hasValue()value()
Gotchas & Edge Cases:
- uses
signal()equality by default; provide custom equality for objectsObject.is() - must be called in constructor or field initializer, not in methods
inject() - always renders
@deferon server (SSR); triggers are ignored server-side@placeholder - value resets when source signal changes; use computation form to preserve previous
linkedSignal() - without phase specification defaults to
afterRenderEffect()which can cause layout thrashingmixedReadWrite
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 中缺少 - 导致不必要的 DOM 重建和性能问题
track - 使用构造函数注入而非 inject() - 冗余代码更多,灵活性更低
- 直接修改 signal 值 - 不会触发更新;使用
signal().push(item)配合扩展运算符.update() - 手动同步信号而非使用 linkedSignal() - 对于依赖型可写状态,使用 (v19+)
linkedSignal() - 使用 resource() 进行变更操作 - /
resource()/rxResource()是只读的;使用 HttpClient 处理 POST/PUT/DELETEhttpResource()
中优先级:
- @defer 用于视口上方组件 - 损害 LCP 和 CLS 核心 Web 指标
- 使用 effect() 处理派生状态 - 改用 或
computed()linkedSignal() - 使用 effect() 处理 DOM 操作 - 使用带阶段参数的
afterRenderEffect() - toSignal() 未指定 initialValue - 若 Observable 未发射值可能导致运行时错误
- 未检查 resource 的 hasValue() - 在访问 前使用
value()作为类型守卫hasValue()
陷阱与边缘情况:
- 默认使用
signal()进行相等性检查;对于对象需提供自定义相等性函数Object.is() - 必须在构造函数或字段初始化器中调用,不能在方法中调用
inject() - 在服务器端(SSR)始终渲染
@defer;触发条件在服务器端被忽略@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 when intentionally using NgModules)
standalone: false(You MUST use , , functions instead of , decorators)
input()output()model()@Input()@Output()(You MUST use function for dependency injection, NOT constructor injection)
inject()(You MUST use , , control flow blocks, NOT , , )
@if@for@switch*ngIf*ngFor*ngSwitch(You MUST use expression in ALL loops)
track@for(You MUST use instead of manual signal synchronization for dependent writable state)
linkedSignal()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(所有 循环必须使用 表达式)
@fortrack(对于依赖型可写状态,必须使用 而非手动同步信号)
linkedSignal()不遵循这些规则将生成遗留 Angular 代码,无法获得性能优化和现代响应式能力的优势。
</critical_reminders>