一键导入
signals-patterns
Angular Signals - Modern reactive state management patterns with computed signals, effects, and interoperability with RxJS
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Angular Signals - Modern reactive state management patterns with computed signals, effects, and interoperability with RxJS
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Proven architectural patterns for building scalable Angular applications in enterprise environments with large teams
Doguhan Uluca's Router-First Architecture - The 7 steps for designing scalable Angular applications by defining routes before components
Essential RxJS operators and patterns for Angular development - transformation, filtering, combination, and error handling operators
Comprehensive guide to Angular bundle optimization, code splitting, tree shaking, and lazy loading strategies
Deep dive into Angular change detection optimization, OnPush strategy, Signals, and Zone.js patterns
Secure authentication and authorization patterns including JWT, OAuth2, guards, and role-based access control
| name | signals-patterns |
| description | Angular Signals - Modern reactive state management patterns with computed signals, effects, and interoperability with RxJS |
Version: Angular 16+
Status: Stable (Developer Preview in 16, Stable in 17+)
Purpose: Modern reactive state management without Zone.js overhead
Signals are Angular's modern approach to reactive state management. Unlike observables, signals are synchronous, glitch-free, and optimized for change detection.
Key Benefits:
import { signal, computed, effect } from '@angular/core';
// Writable signal
const count = signal(0); // number signal
const name = signal('John'); // string signal
const user = signal<User | null>(null); // object signal with null
// Read value (call as function)
console.log(count()); // 0
console.log(name()); // "John"
// Write value
count.set(5); // Set to exact value
name.set('Jane');
// Update value (based on current)
count.update(n => n + 1); // Increment
Computed signals automatically recalculate when dependencies change:
const count = signal(0);
// Derived value
const double = computed(() => count() * 2);
const isEven = computed(() => count() % 2 === 0);
const message = computed(() =>
`Count is ${count()} and ${isEven() ? 'even' : 'odd'}`
);
console.log(double()); // 0
console.log(message()); // "Count is 0 and even"
count.set(3);
console.log(double()); // 6
console.log(message()); // "Count is 3 and odd"
Effects run side effects when signals change:
import { effect } from '@angular/core';
const count = signal(0);
// Effect runs when count changes
effect(() => {
console.log(`Count changed to: ${count()}`);
localStorage.setItem('count', count().toString());
});
count.set(5); // Logs: "Count changed to: 5"
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-todo-list',
standalone: true,
template: `
<input
[value]="newTodo()"
(input)="newTodo.set($any($event.target).value)"
/>
<button (click)="addTodo()">Add</button>
<p>Total: {{ total() }} | Active: {{ active() }} | Completed: {{ completed() }}</p>
@for (todo of todos(); track todo.id) {
<div>
<input
type="checkbox"
[checked]="todo.completed"
(change)="toggle(todo.id)"
/>
<span [class.line-through]="todo.completed">
{{ todo.text }}
</span>
<button (click)="remove(todo.id)">×</button>
</div>
}
`
})
export class TodoListComponent {
// State
todos = signal<Todo[]>([]);
newTodo = signal('');
// Computed
total = computed(() => this.todos().length);
active = computed(() => this.todos().filter(t => !t.completed).length);
completed = computed(() => this.todos().filter(t => t.completed).length);
addTodo() {
if (this.newTodo().trim()) {
this.todos.update(todos => [
...todos,
{
id: Date.now().toString(),
text: this.newTodo(),
completed: false
}
]);
this.newTodo.set('');
}
}
toggle(id: string) {
this.todos.update(todos =>
todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
);
}
remove(id: string) {
this.todos.update(todos => todos.filter(t => t.id !== id));
}
}
@Component({...})
export class ShoppingCartComponent {
items = signal<CartItem[]>([]);
taxRate = signal(0.08);
shippingCost = signal(5.99);
// Computed from items
subtotal = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
// Computed from subtotal and taxRate
tax = computed(() => this.subtotal() * this.taxRate());
// Computed from multiple signals
total = computed(() =>
this.subtotal() + this.tax() + this.shippingCost()
);
// Computed boolean
hasItems = computed(() => this.items().length > 0);
canCheckout = computed(() =>
this.hasItems() && this.total() > 0
);
}
@Component({...})
export class ListComponent {
items = signal<Item[]>([]);
// Add item
addItem(item: Item) {
this.items.update(current => [...current, item]);
}
// Remove item
removeItem(id: string) {
this.items.update(current => current.filter(item => item.id !== id));
}
// Update item
updateItem(id: string, updates: Partial<Item>) {
this.items.update(current =>
current.map(item =>
item.id === id ? { ...item, ...updates } : item
)
);
}
// Sort items
sortBy(key: keyof Item) {
this.items.update(current =>
[...current].sort((a, b) => a[key] > b[key] ? 1 : -1)
);
}
// Filter items
filteredItems = computed(() =>
this.items().filter(item => item.active)
);
}
interface User {
id: string;
name: string;
email: string;
preferences: {
theme: 'light' | 'dark';
language: string;
};
}
@Component({...})
export class UserProfileComponent {
user = signal<User>({
id: '1',
name: 'John',
email: 'john@example.com',
preferences: {
theme: 'light',
language: 'en'
}
});
// Update top-level property
updateName(name: string) {
this.user.update(u => ({ ...u, name }));
}
// Update nested property
updateTheme(theme: 'light' | 'dark') {
this.user.update(u => ({
...u,
preferences: {
...u.preferences,
theme
}
}));
}
// Computed from nested property
isDarkMode = computed(() => this.user().preferences.theme === 'dark');
}
interface LoadingState<T> {
loading: boolean;
data: T | null;
error: string | null;
}
@Component({...})
export class DataComponent {
private http = inject(HttpClient);
state = signal<LoadingState<Product[]>>({
loading: false,
data: null,
error: null
});
// Computed
isLoading = computed(() => this.state().loading);
hasError = computed(() => this.state().error !== null);
hasData = computed(() => this.state().data !== null);
products = computed(() => this.state().data ?? []);
loadData() {
this.state.update(s => ({ ...s, loading: true, error: null }));
this.http.get<Product[]>('/api/products').subscribe({
next: data => this.state.set({ loading: false, data, error: null }),
error: err => this.state.set({ loading: false, data: null, error: err.message })
});
}
}
@Component({
selector: 'app-signup-form',
template: `
<form (submit)="handleSubmit($event)">
<input
type="email"
[value]="email()"
(input)="email.set($any($event.target).value)"
[class.invalid]="emailError()"
/>
@if (emailError()) {
<span class="error">{{ emailError() }}</span>
}
<input
type="password"
[value]="password()"
(input)="password.set($any($event.target).value)"
/>
<button [disabled]="!isValid()">Sign Up</button>
</form>
`
})
export class SignupFormComponent {
// Form fields
email = signal('');
password = signal('');
// Validation
emailError = computed(() => {
const value = this.email();
if (!value) return 'Email is required';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return 'Invalid email format';
}
return null;
});
passwordError = computed(() => {
const value = this.password();
if (!value) return 'Password is required';
if (value.length < 8) return 'Minimum 8 characters';
return null;
});
isValid = computed(() =>
!this.emailError() && !this.passwordError()
);
handleSubmit(event: Event) {
event.preventDefault();
if (this.isValid()) {
console.log({ email: this.email(), password: this.password() });
}
}
}
@Component({...})
export class AutoSaveComponent {
content = signal('');
constructor() {
// Auto-save effect
effect(() => {
const data = this.content();
if (data) {
const timeoutId = setTimeout(() => {
console.log('Auto-saving:', data);
this.save(data);
}, 1000);
// Cleanup function
return () => clearTimeout(timeoutId);
}
});
// Log changes
effect(() => {
console.log('Content length:', this.content().length);
});
}
save(data: string) {
localStorage.setItem('draft', data);
}
}
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
@Component({...})
export class MixedComponent {
// Observable to Signal
private tick$ = interval(1000);
tick = toSignal(this.tick$, { initialValue: 0 });
// Signal to Observable
count = signal(0);
count$ = toObservable(this.count);
constructor() {
// Subscribe to signal as observable
this.count$.subscribe(value => {
console.log('Count changed:', value);
});
}
}
@Component({...})
export class UserComponent {
private http = inject(HttpClient);
userId = signal('123');
// Convert signal to observable, then fetch
user = toSignal(
toObservable(this.userId).pipe(
switchMap(id => this.http.get<User>(`/api/users/${id}`))
),
{ initialValue: null }
);
}
// store.service.ts
import { Injectable, signal, computed } from '@angular/core';
export interface AppState {
user: User | null;
theme: 'light' | 'dark';
notifications: Notification[];
}
@Injectable({ providedIn: 'root' })
export class Store {
// Private state
private state = signal<AppState>({
user: null,
theme: 'light',
notifications: []
});
// Public selectors
user = computed(() => this.state().user);
theme = computed(() => this.state().theme);
notifications = computed(() => this.state().notifications);
unreadCount = computed(() =>
this.state().notifications.filter(n => !n.read).length
);
// Actions
setUser(user: User | null) {
this.state.update(s => ({ ...s, user }));
}
toggleTheme() {
this.state.update(s => ({
...s,
theme: s.theme === 'light' ? 'dark' : 'light'
}));
}
addNotification(notification: Notification) {
this.state.update(s => ({
...s,
notifications: [...s.notifications, notification]
}));
}
}
// Usage in component
@Component({...})
export class AppComponent {
private store = inject(Store);
user = this.store.user;
theme = this.store.theme;
unreadCount = this.store.unreadCount;
logout() {
this.store.setUser(null);
}
}
Don't mutate signal values directly
// ❌ Bad
items().push(newItem);
// ✅ Good
items.update(current => [...current, newItem]);
Don't use effects for derived state
// ❌ Bad - Use computed instead
const count = signal(0);
const double = signal(0);
effect(() => double.set(count() * 2));
// ✅ Good
const double = computed(() => count() * 2);
Don't create signals in loops
Don't read signals in constructors (use ngOnInit or effects)
Use Signals for:
Use RxJS for:
Use Both:
toSignal()toObservable()Signals provide:
Key Takeaway: Signals are the future of Angular state management. Use them for synchronous state, computed values, and reactive UI updates.