angular-signals
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAngular Signals
Angular Signals
Master Angular Signals for building reactive applications with
fine-grained reactivity and improved performance.
掌握Angular Signals,构建具备细粒度响应性和更高性能的响应式应用。
Signal Basics
Signal 基础
Creating and Using Signals
创建和使用Signal
typescript
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<div>
<p>Count: {{ count() }}</p>
<p>Double: {{ doubleCount() }}</p>
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
<button (click)="reset()">Reset</button>
</div>
`
})
export class CounterComponent {
// Writable signal
count = signal(0);
// Computed signal
doubleCount = computed(() => this.count() * 2);
constructor() {
// Effect runs when count changes
effect(() => {
console.log(`Count is: ${this.count()}`);
});
}
increment() {
this.count.update(value => value + 1);
}
decrement() {
this.count.update(value => value - 1);
}
reset() {
this.count.set(0);
}
}typescript
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<div>
<p>Count: {{ count() }}</p>
<p>Double: {{ doubleCount() }}</p>
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
<button (click)="reset()">Reset</button>
</div>
`
})
export class CounterComponent {
// Writable signal
count = signal(0);
// Computed signal
doubleCount = computed(() => this.count() * 2);
constructor() {
// Effect runs when count changes
effect(() => {
console.log(`Count is: ${this.count()}`);
});
}
increment() {
this.count.update(value => value + 1);
}
decrement() {
this.count.update(value => value - 1);
}
reset() {
this.count.set(0);
}
}Signal Methods
Signal 方法
typescript
import { signal } from '@angular/core';
// Create signal
const count = signal(0);
// set - replace value
count.set(5);
// update - transform current value
count.update(value => value + 1);
// mutate - modify object (experimental)
const user = signal({ name: 'John', age: 30 });
user.mutate(value => {
value.age = 31; // Mutate in place
});
// Read value
const current = count(); // Call as functiontypescript
import { signal } from '@angular/core';
// Create signal
const count = signal(0);
// set - replace value
count.set(5);
// update - transform current value
count.update(value => value + 1);
// mutate - modify object (experimental)
const user = signal({ name: 'John', age: 30 });
user.mutate(value => {
value.age = 31; // Mutate in place
});
// Read value
const current = count(); // Call as functionComputed Signals
Computed Signal
Basic Computed
基础Computed
typescript
import { signal, computed } from '@angular/core';
const firstName = signal('John');
const lastName = signal('Doe');
// Computed signal
const fullName = computed(() => {
return `${firstName()} ${lastName()}`;
});
console.log(fullName()); // John Doe
firstName.set('Jane');
console.log(fullName()); // Jane Doe (automatically updates)typescript
import { signal, computed } from '@angular/core';
const firstName = signal('John');
const lastName = signal('Doe');
// Computed signal
const fullName = computed(() => {
return `${firstName()} ${lastName()}`;
});
console.log(fullName()); // John Doe
firstName.set('Jane');
console.log(fullName()); // Jane Doe (automatically updates)Complex Computed
复杂Computed
typescript
interface Product {
id: number;
name: string;
price: number;
quantity: number;
}
@Component({
selector: 'app-cart'
})
export class CartComponent {
items = signal<Product[]>([]);
// Computed: total items
itemCount = computed(() => {
return this.items().reduce((sum, item) => sum + item.quantity, 0);
});
// Computed: subtotal
subtotal = computed(() => {
return this.items().reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
});
// Computed: tax
tax = computed(() => this.subtotal() * 0.08);
// Computed: total
total = computed(() => this.subtotal() + this.tax());
// Computed: formatted total
formattedTotal = computed(() => {
return `$${this.total().toFixed(2)}`;
});
}typescript
interface Product {
id: number;
name: string;
price: number;
quantity: number;
}
@Component({
selector: 'app-cart'
})
export class CartComponent {
items = signal<Product[]>([]);
// Computed: total items
itemCount = computed(() => {
return this.items().reduce((sum, item) => sum + item.quantity, 0);
});
// Computed: subtotal
subtotal = computed(() => {
return this.items().reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
});
// Computed: tax
tax = computed(() => this.subtotal() * 0.08);
// Computed: total
total = computed(() => this.subtotal() + this.tax());
// Computed: formatted total
formattedTotal = computed(() => {
return `$${this.total().toFixed(2)}`;
});
}Chained Computed
链式Computed
typescript
const count = signal(1);
const doubled = computed(() => count() * 2);
const quadrupled = computed(() => doubled() * 2);
const formatted = computed(() => `Count: ${quadrupled()}`);
console.log(formatted()); // Count: 4
count.set(2);
console.log(formatted()); // Count: 8typescript
const count = signal(1);
const doubled = computed(() => count() * 2);
const quadrupled = computed(() => doubled() * 2);
const formatted = computed(() => `Count: ${quadrupled()}`);
console.log(formatted()); // Count: 4
count.set(2);
console.log(formatted()); // Count: 8Effects
Effect
Basic Effects
基础Effect
typescript
import { Component, signal, effect } from '@angular/core';
@Component({
selector: 'app-logger'
})
export class LoggerComponent {
count = signal(0);
constructor() {
// Effect runs when count changes
effect(() => {
console.log(`Count changed to: ${this.count()}`);
});
}
increment() {
this.count.update(v => v + 1); // Triggers effect
}
}typescript
import { Component, signal, effect } from '@angular/core';
@Component({
selector: 'app-logger'
})
export class LoggerComponent {
count = signal(0);
constructor() {
// Effect runs when count changes
effect(() => {
console.log(`Count changed to: ${this.count()}`);
});
}
increment() {
this.count.update(v => v + 1); // Triggers effect
}
}Effect Cleanup
Effect 清理
typescript
import { effect } from '@angular/core';
const count = signal(0);
effect((onCleanup) => {
const timer = setInterval(() => {
console.log(count());
}, 1000);
// Cleanup function
onCleanup(() => {
clearInterval(timer);
});
});typescript
import { effect } from '@angular/core';
const count = signal(0);
effect((onCleanup) => {
const timer = setInterval(() => {
console.log(count());
}, 1000);
// Cleanup function
onCleanup(() => {
clearInterval(timer);
});
});Conditional Effects
条件Effect
typescript
import { effect, signal } from '@angular/core';
const enabled = signal(true);
const count = signal(0);
effect(() => {
// Only run if enabled
if (!enabled()) return;
console.log(`Count: ${count()}`);
});typescript
import { effect, signal } from '@angular/core';
const enabled = signal(true);
const count = signal(0);
effect(() => {
// Only run if enabled
if (!enabled()) return;
console.log(`Count: ${count()}`);
});Signal Inputs
Signal 输入
Component Inputs as Signals
作为Signal的组件输入
typescript
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-user-profile',
template: `
<div>
<h2>{{ displayName() }}</h2>
<p>Age: {{ age() }}</p>
<p>Is adult: {{ isAdult() }}</p>
</div>
`
})
export class UserProfileComponent {
// Signal inputs (Angular 17.1+)
firstName = input.required<string>();
lastName = input.required<string>();
age = input(0); // Optional with default
// Computed from inputs
displayName = computed(() =>
`${this.firstName()} ${this.lastName()}`
);
isAdult = computed(() => this.age() >= 18);
}
// Usage
<app-user-profile
[firstName]="'John'"
[lastName]="'Doe'"
[age]="30"
/>typescript
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-user-profile',
template: `
<div>
<h2>{{ displayName() }}</h2>
<p>Age: {{ age() }}</p>
<p>Is adult: {{ isAdult() }}</p>
</div>
`
})
export class UserProfileComponent {
// Signal inputs (Angular 17.1+)
firstName = input.required<string>();
lastName = input.required<string>();
age = input(0); // Optional with default
// Computed from inputs
displayName = computed(() =>
`${this.firstName()} ${this.lastName()}`
);
isAdult = computed(() => this.age() >= 18);
}
// Usage
<app-user-profile
[firstName]="'John'"
[lastName]="'Doe'"
[age]="30"
/>Transform Input Signals
转换Signal输入
typescript
import { Component, input } from '@angular/core';
@Component({
selector: 'app-formatted-text'
})
export class FormattedTextComponent {
// Transform input
text = input('', {
transform: (value: string) => value.toUpperCase()
});
// Alias input
label = input('', { alias: 'labelText' });
}
// Usage
<app-formatted-text
[text]="'hello'"
[labelText]="'Name'"
/>typescript
import { Component, input } from '@angular/core';
@Component({
selector: 'app-formatted-text'
})
export class FormattedTextComponent {
// Transform input
text = input('', {
transform: (value: string) => value.toUpperCase()
});
// Alias input
label = input('', { alias: 'labelText' });
}
// Usage
<app-formatted-text
[text]="'hello'"
[labelText]="'Name'"
/>Signal Outputs
Signal 输出
Component Outputs as Signals
作为Signal的组件输出
typescript
import { Component, output } from '@angular/core';
@Component({
selector: 'app-button',
template: `
<button (click)="handleClick()">
{{ label() }}
</button>
`
})
export class ButtonComponent {
label = input('Click me');
// Signal output (Angular 17.1+)
clicked = output<void>();
valueChanged = output<number>();
private clickCount = signal(0);
handleClick() {
this.clickCount.update(v => v + 1);
this.clicked.emit();
this.valueChanged.emit(this.clickCount());
}
}
// Usage
<app-button
(clicked)="onClicked()"
(valueChanged)="onValueChanged($event)"
/>typescript
import { Component, output } from '@angular/core';
@Component({
selector: 'app-button',
template: `
<button (click)="handleClick()">
{{ label() }}
</button>
`
})
export class ButtonComponent {
label = input('Click me');
// Signal output (Angular 17.1+)
clicked = output<void>();
valueChanged = output<number>();
private clickCount = signal(0);
handleClick() {
this.clickCount.update(v => v + 1);
this.clicked.emit();
this.valueChanged.emit(this.clickCount());
}
}
// Usage
<app-button
(clicked)="onClicked()"
(valueChanged)="onValueChanged($event)"
/>Signal Queries
Signal 查询
ViewChild with Signals
结合Signal使用ViewChild
typescript
import { Component, viewChild, ElementRef, afterNextRender } from '@angular/core';
@Component({
selector: 'app-input-focus',
template: `
<input #inputElement type="text" />
<button (click)="focusInput()">Focus</button>
`
})
export class InputFocusComponent {
// Signal-based viewChild
inputElement = viewChild<ElementRef>('inputElement');
constructor() {
afterNextRender(() => {
// Access element after render
const element = this.inputElement()?.nativeElement;
if (element) {
element.focus();
}
});
}
focusInput() {
this.inputElement()?.nativeElement.focus();
}
}typescript
import { Component, viewChild, ElementRef, afterNextRender } from '@angular/core';
@Component({
selector: 'app-input-focus',
template: `
<input #inputElement type="text" />
<button (click)="focusInput()">Focus</button>
`
})
export class InputFocusComponent {
// Signal-based viewChild
inputElement = viewChild<ElementRef>('inputElement');
constructor() {
afterNextRender(() => {
// Access element after render
const element = this.inputElement()?.nativeElement;
if (element) {
element.focus();
}
});
}
focusInput() {
this.inputElement()?.nativeElement.focus();
}
}ViewChildren with Signals
结合Signal使用ViewChildren
typescript
import { Component, viewChildren, ElementRef } from '@angular/core';
@Component({
selector: 'app-list',
template: `
<div #item *ngFor="let item of items()">
{{ item }}
</div>
<p>Item count: {{ itemElements().length }}</p>
`
})
export class ListComponent {
items = signal(['A', 'B', 'C']);
// Signal-based viewChildren
itemElements = viewChildren<ElementRef>('item');
logItemCount() {
console.log(`Count: ${this.itemElements().length}`);
}
}typescript
import { Component, viewChildren, ElementRef } from '@angular/core';
@Component({
selector: 'app-list',
template: `
<div #item *ngFor="let item of items()">
{{ item }}
</div>
<p>Item count: {{ itemElements().length }}</p>
`
})
export class ListComponent {
items = signal(['A', 'B', 'C']);
// Signal-based viewChildren
itemElements = viewChildren<ElementRef>('item');
logItemCount() {
console.log(`Count: ${this.itemElements().length}`);
}
}ContentChild with Signals
结合Signal使用ContentChild
typescript
import { Component, contentChild, Directive } from '@angular/core';
@Directive({
selector: '[appHeader]'
})
export class HeaderDirective {}
@Component({
selector: 'app-card',
template: `
<div class="card">
<ng-content select="[appHeader]" />
<ng-content />
<p *ngIf="hasHeader()">Has custom header</p>
</div>
`
})
export class CardComponent {
// Signal-based contentChild
header = contentChild(HeaderDirective);
hasHeader = computed(() => !!this.header());
}
// Usage
<app-card>
<h2 appHeader>Title</h2>
<p>Content</p>
</app-card>typescript
import { Component, contentChild, Directive } from '@angular/core';
@Directive({
selector: '[appHeader]'
})
export class HeaderDirective {}
@Component({
selector: 'app-card',
template: `
<div class="card">
<ng-content select="[appHeader]" />
<ng-content />
<p *ngIf="hasHeader()">Has custom header</p>
</div>
`
})
export class CardComponent {
// Signal-based contentChild
header = contentChild(HeaderDirective);
hasHeader = computed(() => !!this.header());
}
// Usage
<app-card>
<h2 appHeader>Title</h2>
<p>Content</p>
</app-card>Signals vs Observables
Signal vs Observable
When to Use Signals
何时使用Signal
typescript
// Use signals for synchronous state
@Component({
selector: 'app-counter'
})
export class CounterComponent {
count = signal(0); // Signal for synchronous state
increment() {
this.count.update(v => v + 1);
}
}typescript
// Use signals for synchronous state
@Component({
selector: 'app-counter'
})
export class CounterComponent {
count = signal(0); // Signal for synchronous state
increment() {
this.count.update(v => v + 1);
}
}When to Use Observables
何时使用Observable
typescript
// Use observables for async operations
@Component({
selector: 'app-user-list'
})
export class UserListComponent {
private http = inject(HttpClient);
users$: Observable<User[]> = this.http.get<User[]>('/api/users');
}typescript
// Use observables for async operations
@Component({
selector: 'app-user-list'
})
export class UserListComponent {
private http = inject(HttpClient);
users$: Observable<User[]> = this.http.get<User[]>('/api/users');
}Combining Signals and Observables
结合Signal和Observable
typescript
import { Component, signal } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, switchMap } from 'rxjs/operators';
@Component({
selector: 'app-search'
})
export class SearchComponent {
private http = inject(HttpClient);
// Signal for search query
searchQuery = signal('');
// Convert signal to observable
searchQuery$ = toObservable(this.searchQuery);
// Use observable operators
results$ = this.searchQuery$.pipe(
debounceTime(300),
switchMap(query => this.http.get(`/api/search?q=${query}`))
);
// Convert back to signal
results = toSignal(this.results$, { initialValue: [] });
}typescript
import { Component, signal } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, switchMap } from 'rxjs/operators';
@Component({
selector: 'app-search'
})
export class SearchComponent {
private http = inject(HttpClient);
// Signal for search query
searchQuery = signal('');
// Convert signal to observable
searchQuery$ = toObservable(this.searchQuery);
// Use observable operators
results$ = this.searchQuery$.pipe(
debounceTime(300),
switchMap(query => this.http.get(`/api/search?q=${query}`))
);
// Convert back to signal
results = toSignal(this.results$, { initialValue: [] });
}toSignal and toObservable
toSignal 和 toObservable
toSignal - Observable to Signal
toObservable - Observable转Signal
typescript
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-user-list',
template: `
<div *ngIf="users()">
<div *ngFor="let user of users()">
{{ user.name }}
</div>
</div>
`
})
export class UserListComponent {
private http = inject(HttpClient);
// Convert observable to signal
users = toSignal(
this.http.get<User[]>('/api/users'),
{ initialValue: [] as User[] }
);
}typescript
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-user-list',
template: `
<div *ngIf="users()">
<div *ngFor="let user of users()">
{{ user.name }}
</div>
</div>
`
})
export class UserListComponent {
private http = inject(HttpClient);
// Convert observable to signal
users = toSignal(
this.http.get<User[]>('/api/users'),
{ initialValue: [] as User[] }
);
}toObservable - Signal to Observable
toObservable - Signal转Observable
typescript
import { Component, signal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { debounceTime } from 'rxjs/operators';
@Component({
selector: 'app-search'
})
export class SearchComponent {
searchTerm = signal('');
// Convert signal to observable
searchTerm$ = toObservable(this.searchTerm);
constructor() {
// Use observable operators
this.searchTerm$.pipe(
debounceTime(300)
).subscribe(term => {
console.log('Searching for:', term);
});
}
}typescript
import { Component, signal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { debounceTime } from 'rxjs/operators';
@Component({
selector: 'app-search'
})
export class SearchComponent {
searchTerm = signal('');
// Convert signal to observable
searchTerm$ = toObservable(this.searchTerm);
constructor() {
// Use observable operators
this.searchTerm$.pipe(
debounceTime(300)
).subscribe(term => {
console.log('Searching for:', term);
});
}
}Signal Equality and Change Detection
Signal 相等性与变更检测
Custom Equality Function
自定义相等性函数
typescript
import { signal } from '@angular/core';
interface User {
id: number;
name: string;
}
// Custom equality check
const user = signal<User>(
{ id: 1, name: 'John' },
{
equal: (a, b) => a.id === b.id // Only compare IDs
}
);
user.set({ id: 1, name: 'Jane' }); // No update (same ID)
user.set({ id: 2, name: 'John' }); // Updates (different ID)typescript
import { signal } from '@angular/core';
interface User {
id: number;
name: string;
}
// Custom equality check
const user = signal<User>(
{ id: 1, name: 'John' },
{
equal: (a, b) => a.id === b.id // Only compare IDs
}
);
user.set({ id: 1, name: 'Jane' }); // No update (same ID)
user.set({ id: 2, name: 'John' }); // Updates (different ID)Zone-less Change Detection
无Zone变更检测
typescript
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
@Component({
selector: 'app-counter',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">+</button>
`
})
export class CounterComponent {
count = signal(0);
increment() {
// Signal updates trigger change detection automatically
this.count.update(v => v + 1);
}
}typescript
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
@Component({
selector: 'app-counter',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">+</button>
`
})
export class CounterComponent {
count = signal(0);
increment() {
// Signal updates trigger change detection automatically
this.count.update(v => v + 1);
}
}Migration from Observables
从Observable迁移
Before - Observables
之前 - 使用Observable
typescript
import { Component } from '@angular/core';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'app-cart'
})
export class CartComponentOld {
private items$ = new BehaviorSubject<Product[]>([]);
private discount$ = new BehaviorSubject<number>(0);
total$ = combineLatest([this.items$, this.discount$]).pipe(
map(([items, discount]) => {
const subtotal = items.reduce((sum, item) =>
sum + item.price * item.quantity, 0
);
return subtotal * (1 - discount);
})
);
addItem(item: Product) {
this.items$.next([...this.items$.value, item]);
}
}typescript
import { Component } from '@angular/core';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'app-cart'
})
export class CartComponentOld {
private items$ = new BehaviorSubject<Product[]>([]);
private discount$ = new BehaviorSubject<number>(0);
total$ = combineLatest([this.items$, this.discount$]).pipe(
map(([items, discount]) => {
const subtotal = items.reduce((sum, item) =>
sum + item.price * item.quantity, 0
);
return subtotal * (1 - discount);
})
);
addItem(item: Product) {
this.items$.next([...this.items$.value, item]);
}
}After - Signals
之后 - 使用Signal
typescript
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-cart'
})
export class CartComponent {
items = signal<Product[]>([]);
discount = signal(0);
total = computed(() => {
const subtotal = this.items().reduce((sum, item) =>
sum + item.price * item.quantity, 0
);
return subtotal * (1 - this.discount());
});
addItem(item: Product) {
this.items.update(items => [...items, item]);
}
}typescript
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-cart'
})
export class CartComponent {
items = signal<Product[]>([]);
discount = signal(0);
total = computed(() => {
const subtotal = this.items().reduce((sum, item) =>
sum + item.price * item.quantity, 0
);
return subtotal * (1 - this.discount());
});
addItem(item: Product) {
this.items.update(items => [...items, item]);
}
}When to Use This Skill
何时使用该技能
Use angular-signals when building modern, production-ready
applications that require:
- Fine-grained reactivity without RxJS
- Simpler state management
- Zone-less change detection
- Better performance for synchronous state
- Cleaner component code
- Angular 16+ applications
- Migrating from observables for sync state
- Component input/output as signals
在构建现代、生产级应用时使用Angular Signals,当你需要:
- 无需RxJS的细粒度响应性
- 更简单的状态管理
- 无Zone变更检测
- 同步状态的更优性能
- 更简洁的组件代码
- Angular 16+应用程序
- 将同步状态从Observable迁移
- 以Signal形式实现组件输入/输出
Signal Best Practices
Signal 最佳实践
- Use signals for synchronous state - Perfect for component state
- Use computed for derived values - Automatic dependency tracking
- Prefer signals over observables for state - Simpler mental model
- Use effects sparingly - Only for side effects
- Signal inputs for better types - Type-safe component props
- Combine with observables when needed - Use toSignal/toObservable
- Use custom equality for objects - Optimize updates
- Leverage zone-less change detection - Better performance
- Keep signals focused - Small, single-purpose signals
- Use mutate carefully - Prefer update for immutability
- 使用Signal管理同步状态 - 非常适合组件状态
- 使用Computed处理派生值 - 自动依赖追踪
- 优先使用Signal而非Observable管理状态 - 更简单的心智模型
- 谨慎使用Effect - 仅用于处理副作用
- 使用Signal输入获得更优类型 - 类型安全的组件属性
- 必要时结合Observable使用 - 使用toSignal/toObservable
- 为对象使用自定义相等性 - 优化更新逻辑
- 利用无Zone变更检测 - 提升性能
- 保持Signal聚焦 - 小巧、单一用途的Signal
- 谨慎使用mutate - 优先使用update保证不可变性
Signal Pitfalls
Signal 常见陷阱
- Overusing effects - Can create complex dependencies
- Mutating signal values directly - Use update/mutate methods
- Not understanding equality - Objects update by reference
- Mixing patterns - Choose signals OR observables per feature
- Effects in loops - Can cause performance issues
- Not cleaning up effects - Memory leaks
- Computed with side effects - Should be pure functions
- Reading signals outside tracking context - Won't track dependencies
- Complex effect dependencies - Hard to debug
- Forgetting to call signal - vs
countcount()
- 过度使用Effect - 可能创建复杂依赖
- 直接修改Signal值 - 使用update/mutate方法
- 不理解相等性逻辑 - 对象按引用更新
- 混合使用模式 - 每个功能选择Signal或Observable其一
- 在循环中使用Effect - 可能导致性能问题
- 未清理Effect - 造成内存泄漏
- 在Computed中包含副作用 - Computed应是纯函数
- 在追踪上下文外读取Signal - 无法追踪依赖
- 复杂的Effect依赖 - 难以调试
- 忘记调用Signal - 与
count混淆count()
Advanced Signal Patterns
高级Signal模式
State Management Pattern
状态管理模式
typescript
import { signal, computed } from '@angular/core';
interface TodoState {
items: Todo[];
filter: 'all' | 'active' | 'completed';
}
@Injectable({
providedIn: 'root'
})
export class TodoStore {
// Private state
private state = signal<TodoState>({
items: [],
filter: 'all'
});
// Public selectors
items = computed(() => this.state().items);
filter = computed(() => this.state().filter);
filteredItems = computed(() => {
const items = this.items();
const filter = this.filter();
switch (filter) {
case 'active':
return items.filter(item => !item.completed);
case 'completed':
return items.filter(item => item.completed);
default:
return items;
}
});
// Actions
addTodo(text: string) {
this.state.update(state => ({
...state,
items: [...state.items, { id: Date.now(), text, completed: false }]
}));
}
toggleTodo(id: number) {
this.state.update(state => ({
...state,
items: state.items.map(item =>
item.id === id ? { ...item, completed: !item.completed } : item
)
}));
}
setFilter(filter: TodoState['filter']) {
this.state.update(state => ({ ...state, filter }));
}
}typescript
import { signal, computed } from '@angular/core';
interface TodoState {
items: Todo[];
filter: 'all' | 'active' | 'completed';
}
@Injectable({
providedIn: 'root'
})
export class TodoStore {
// Private state
private state = signal<TodoState>({
items: [],
filter: 'all'
});
// Public selectors
items = computed(() => this.state().items);
filter = computed(() => this.state().filter);
filteredItems = computed(() => {
const items = this.items();
const filter = this.filter();
switch (filter) {
case 'active':
return items.filter(item => !item.completed);
case 'completed':
return items.filter(item => item.completed);
default:
return items;
}
});
// Actions
addTodo(text: string) {
this.state.update(state => ({
...state,
items: [...state.items, { id: Date.now(), text, completed: false }]
}));
}
toggleTodo(id: number) {
this.state.update(state => ({
...state,
items: state.items.map(item =>
item.id === id ? { ...item, completed: !item.completed } : item
)
}));
}
setFilter(filter: TodoState['filter']) {
this.state.update(state => ({ ...state, filter }));
}
}Signal-based Forms
基于Signal的表单
typescript
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-login-form'
})
export class LoginFormComponent {
email = signal('');
password = signal('');
emailError = computed(() => {
const email = this.email();
if (!email) return 'Email is required';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return 'Invalid email format';
}
return null;
});
passwordError = computed(() => {
const password = this.password();
if (!password) return 'Password is required';
if (password.length < 8) {
return 'Password must be at least 8 characters';
}
return null;
});
isValid = computed(() =>
!this.emailError() && !this.passwordError() &&
this.email() && this.password()
);
submit() {
if (!this.isValid()) return;
// Submit form
}
}typescript
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-login-form'
})
export class LoginFormComponent {
email = signal('');
password = signal('');
emailError = computed(() => {
const email = this.email();
if (!email) return '邮箱为必填项';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return '邮箱格式无效';
}
return null;
});
passwordError = computed(() => {
const password = this.password();
if (!password) return '密码为必填项';
if (password.length < 8) {
return '密码长度至少为8位';
}
return null;
});
isValid = computed(() =>
!this.emailError() && !this.passwordError() &&
this.email() && this.password()
);
submit() {
if (!this.isValid()) return;
// 提交表单
}
}