Angular シグナル: Angular における応答性の未来
Angular 16 では、チームはフレームワークの歴史の中で最も重要なイノベーションの 1 つを導入しました。 私は 信号。このリアクティブなプリミティブは、根本的なパラダイムシフトを表しています。 Angular を Zone.js ベースの変更検出システムから きめ細かい応答性、実際に影響を受けるコンポーネントのみ 状態の変化が更新されます。
このシリーズの最初の記事では モダンアンギュラー シグナルについて詳しく見ていきます。 理論から実践へ、単純な反応状態の作成からすぐに使える高度なパターンまで 生産。すでに RxJS と Zone.js に精通している場合は、Signals によってそれがどのように簡単になるかがわかるでしょう。 優れたパフォーマンスを維持しながら、大幅な状態管理を実現します。
最新の Angular シリーズの概要
| # | アイテム | 集中 |
|---|---|---|
| 1 | あなたはここにいます - Angular Signals | きめ細かい応答性 |
| 2 | ゾーンレスの変更検出 | Zone.jsを削除する |
| 3 | 新しいテンプレート @if、@for、@defer | 最新の制御フロー |
| 4 | スタンドアロンコンポーネント | NgModule を使用しないアーキテクチャ |
| 5 | 信号形式 | シグナルを使用したレスポンシブフォーム |
| 6 | SSR と増分水分補給 | サーバーサイドレンダリング |
| 7 | Angular におけるコア Web バイタル | パフォーマンスと指標 |
| 8 | 角度のある PWA | プログレッシブ Web アプリ |
| 9 | 高度な依存関係の注入 | DIツリーシェイク可能 |
| 10 | Angular 17 から 21 への移行 | 移行ガイド |
なぜシグナルなのか? Zone.js の問題
シグナルが導入された理由を理解するには、まず問題を理解する必要があります 彼らが解決するということ。 Angular は当初から、 ゾーン.js 迎撃する 非同期イベント (クリック、タイマー、HTTP リクエスト) と自動的にループをトリガーします。 変化検出。このアプローチは便利ですが、次のような重大な制限があります。
Zone.js の制限事項
- グローバルオーバーヘッド: Zone.js はすべてのブラウザー非同期 API (setTimeout、Promise、addEventListener) にパッチを適用し、バンドルに重みを追加します (最大 13KB の縮小)
- 変更検出が広すぎる: 各非同期イベントは、状態が変化していない場合でも、コンポーネント ツリー全体のチェックをトリガーします。
- 非互換性: 一部のサードパーティ ライブラリは Zone.js で適切に動作しません (Web コンポーネント、一部のネイティブ ライブラリ)
- 複雑なデバッグ: スタック トレースが Zone.js フレームによって汚染され、デバッグが困難になる
- スケーラビリティ: 数百のコンポーネントを含むアプリケーションでは、グローバルな変更の検出がボトルネックになります
Signal は、次のシステムを導入することでこれらの問題を解決します。 グラフベースの応答性: 各シグナルは、どの消費者が依存しているのか、またその値がいつ変更されるのかを正確に把握しています。 それらの消費者のみが通知され、更新されます。ポーリングやグローバルチェックはありません。
Signals と Zone.js: 比較
| 待ってます | ゾーン.js | 信号 |
|---|---|---|
| 粒度 | コンポーネントツリー全体 | テンプレート内の単一バインディング |
| トリガー | 任意の非同期イベント | 値が変化した場合のみ |
| バンドルサイズ | +13KB (zone.js) | 追加0KB |
| トラッキング | 暗黙的 (モンキーパッチ) | 明示的 (依存関係グラフ) |
| デバッグ | 汚染されたスタック トレース | クリーンなスタックトレース |
| 遅延評価 | No | はい (計算済み) |
信号の構造
Un 信号 これは値のリアクティブなラッパーです。と考えることができます。
値が変更されたときに消費者に自動的に通知する変数。機能
signal() を作成する WritableSignal、または可能性のあるシグナル
読み書き。
信号の作成と読み取り
import { signal } from '@angular/core';
// Creazione di un WritableSignal con valore iniziale
const count = signal(0); // WritableSignal<number>
const name = signal('Angular'); // WritableSignal<string>
const items = signal<string[]>([]); // Tipo esplicito con generics
// LEGGERE il valore: chiama il signal come funzione
console.log(count()); // 0
console.log(name()); // 'Angular'
console.log(items()); // []
// SCRIVERE un nuovo valore: usa .set()
count.set(5);
console.log(count()); // 5
// AGGIORNARE basandosi sul valore corrente: usa .update()
count.update(current => current + 1);
console.log(count()); // 6
// AGGIORNARE oggetti/array: usa .update() per immutabilita
items.update(list => [...list, 'Nuovo elemento']);
console.log(items()); // ['Nuovo elemento']
なぜ関数呼び出しを使用して読み取るのですか?
構文 count() それは単なるスタイル上の選択ではありません。それはメカニズムです。
Angular は次のことを可能にします 依存関係を自動的に追跡する。いつ
内のシグナルを読み取る computed() または effect()、角のある
その読み取りを依存関係としてログに記録します。次のような単純なプロパティを使用した場合
count.value、この自動追跡は不可能です。
WritableSignal と ReadonlySignal の比較
Angular は 2 種類のシグナルを区別します。あ WritableSignal メソッドを公開します
.set() e .update()、一方、 Signal (読み取り専用信号)
読み取りのみを許可します。この区別はカプセル化にとって重要です。
import { signal, Signal, WritableSignal } from '@angular/core';
// Signal privato e scrivibile nel servizio
private _counter: WritableSignal<number> = signal(0);
// Esponi solo la versione read-only ai consumatori
readonly counter: Signal<number> = this._counter.asReadonly();
// Metodo pubblico controllato per modificare lo stato
increment(): void {
this._counter.update(v => v + 1);
}
// I consumatori possono leggere ma NON scrivere
// this.counterService.counter() // OK - lettura
// this.counterService.counter.set(5) // ERRORE - non esiste .set()
平等と通知
デフォルトでは、シグナルは値が変更された場合にのみ消費者に通知します。
Object.is()。オプションを使用して比較をカスタマイズできます equal:
// Signal con confronto personalizzato
const user = signal(
{ name: 'Federico', age: 30 },
{
equal: (a, b) => a.name === b.name && a.age === b.age
}
);
// Questo NON notifica i consumatori (stesso contenuto)
user.set({ name: 'Federico', age: 30 });
// Questo notifica i consumatori (contenuto diverso)
user.set({ name: 'Federico', age: 31 });
計算された信号: 派生状態
機能 computed() 値が次のようなシグナルを作成します。 自動的に導き出される
他の信号から。これは計算されたプロパティとリアクティブに相当し、値が返されます。
依存関係の 1 つが変更された場合にのみ再計算され、結果が保存されます。
(キャッシュされます) 次の更新まで。
import { signal, computed } from '@angular/core';
const firstName = signal('Federico');
const lastName = signal('Calo');
const age = signal(30);
// Computed: ricalcolato SOLO quando firstName o lastName cambiano
const fullName = computed(() => `${firstName()} ${lastName()}`);
console.log(fullName()); // 'Federico Calo'
// Computed con logica condizionale
const canVote = computed(() => age() >= 18);
console.log(canVote()); // true
// Computed che dipende da altri computed (catena)
const greeting = computed(() =>
`Ciao ${fullName()}, ${canVote() ? 'puoi votare' : 'non puoi votare'}`
);
console.log(greeting()); // 'Ciao Federico Calo, puoi votare'
// Quando cambia firstName, fullName e greeting si aggiornano
// ma canVote NO (non dipende da firstName)
firstName.set('Marco');
console.log(fullName()); // 'Marco Calo'
console.log(greeting()); // 'Ciao Marco Calo, puoi votare'
computed() の主な機能
- 遅延評価: 値は、依存関係が変更されたときではなく、誰かがそれを読んだときにのみ計算されます。
- メモ化: 依存関係が変更されていない場合、読み取りは再計算せずにキャッシュされた値を返します。
- 自動追跡: 依存関係は導出関数の実行中に検出されます。
- 動的な依存関係: ロジックに
if、そのステップで実際に読み取られた依存関係のみが追跡されます。 - 読み取り専用: 計算されたシグナルにはメソッドがありません
.set()o.update()
警告: computed() には副作用はありません
渡される関数 computed() そうでなければなりません 純粋な:そんなはずはない
他のシグナルの変更、HTTP 呼び出しの実行、DOM への書き込み、または操作の実行
副作用あり。 Angular では、いつでも関数を再実行できます。
値が変更されたかどうかを確認します。副作用については、次を使用してください。 effect().
効果: 反応性の副作用
機能 effect() performs a side effect every time the Signals are read
その中で値が変わります。 It is the ideal mechanism to synchronize the status of
Signals with the outside world: logging, localStorage, analytics, DOM manipulation.
import { signal, effect, Component } from '@angular/core';
@Component({
selector: 'app-logger',
standalone: true,
template: `<button (click)="increment()">Count: {{ count() }}</button>`
})
export class LoggerComponent {
count = signal(0);
constructor() {
// L'effect viene eseguito immediatamente e poi ogni volta
// che count() cambia valore
effect(() => {
console.log(`Il contatore vale: ${this.count()}`);
// Output: "Il contatore vale: 0" (subito)
// Output: "Il contatore vale: 1" (dopo click)
});
}
increment() {
this.count.update(v => v + 1);
}
}
エフェクトのクリーンアップ
エフェクトが再生または破棄されると、リソースをクリーンアップする必要がある場合があります。
タイマー、サブスクリプション、リスナーなど。 Angular はコールバックを提供します onCleanup
この目的のために:
import { signal, effect } from '@angular/core';
const pollingInterval = signal(5000);
effect((onCleanup) => {
const interval = pollingInterval();
const timerId = setInterval(() => {
console.log('Polling dati dal server...');
}, interval);
// Questa funzione viene chiamata PRIMA della prossima esecuzione
// e quando il componente viene distrutto
onCleanup(() => {
clearInterval(timerId);
console.log('Timer pulito');
});
});
エフェクトのインジェクションコンテキスト
effect() 内で呼び出す必要があります インジェクションコンテキスト、
つまり、コンポーネント、ディレクティブ、またはサービスのコンストラクター内、または内部
ファクトリー関数 inject()。外部でエフェクトを作成する必要がある場合
コンストラクターから明示的に渡すInjector:
import { signal, effect, inject, Injector, Component } from '@angular/core';
@Component({
selector: 'app-deferred',
standalone: true,
template: `<button (click)="startTracking()">Avvia tracking</button>`
})
export class DeferredEffectComponent {
private injector = inject(Injector);
data = signal('');
startTracking() {
// Passiamo l'injector esplicitamente
effect(() => {
console.log('Dati aggiornati:', this.data());
}, { injector: this.injector });
}
}
信号入力: 新しい @Input()
Angular 17.1 以降、デコレータ @Input() 現代的なオルタナティブをベースにした
シグナル上: 関数 input()。信号入力は次のとおりです。 読み取り専用
リアクティブ グラフにシームレスに統合し、それらを直接使用できるようにします。
computed() e effect().
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<div class="card">
<h3>{{ displayName() }}</h3>
<span class="badge" [class.active]="isActive()">
{{ isActive() ? 'Attivo' : 'Inattivo' }}
</span>
</div>
`
})
export class UserCardComponent {
// Input opzionale con valore di default
name = input('Anonimo'); // InputSignal<string>
// Input obbligatorio (il parent DEVE fornirlo)
userId = input.required<number>(); // InputSignal<number>
// Input con alias (nome diverso nel template del parent)
active = input(false, { alias: 'isUserActive' });
// Computed che dipendono dagli input - tutto reattivo!
displayName = computed(() =>
`${this.name()} (ID: ${this.userId()})`
);
isActive = computed(() => this.active());
}
// Uso nel parent:
// <app-user-card [name]="'Federico'" [userId]="42" [isUserActive]="true" />
@Input() に対する input() の利点
- タイプセーフ:
input.required()親が値を指定しない場合、コンパイル エラーが生成されます。 - ネイティブの応答性: 値はシグナルなので、直接動作します。
computed()eeffect() - OnChange なし: もう実装する必要はありません
ngOnChanges入力の変化に反応する - 統合された変換: オプションを使用して入力値を変換できます
transform - 不変性: 信号入力は読み取り専用であり、子コンポーネントでの偶発的な変更を防ぎます。
入力変換
import { Component, input, booleanAttribute, numberAttribute } from '@angular/core';
@Component({
selector: 'app-settings',
standalone: true,
template: `
<p>Colonne: {{ columns() }}</p>
<p>Disabilitato: {{ disabled() }}</p>
`
})
export class SettingsComponent {
// Converte automaticamente la stringa "3" nel numero 3
columns = input(1, { transform: numberAttribute });
// Converte la presenza dell'attributo in boolean
// <app-settings disabled /> => true
disabled = input(false, { transform: booleanAttribute });
}
信号出力: 新しい @Output()
入力と同様に、Angular では output() 現代的な代替品として
デコレーターに @Output()。機能 output() を返します
OutputEmitterRef メソッドを使って .emit() イベントを発行します。
import { Component, input, output } from '@angular/core';
@Component({
selector: 'app-todo-item',
standalone: true,
template: `
<div class="todo">
<span>{{ text() }}</span>
<button (click)="onComplete()">Completa</button>
<button (click)="onDelete()">Elimina</button>
</div>
`
})
export class TodoItemComponent {
// Input
text = input.required<string>();
todoId = input.required<number>();
// Output - tipizzati e type-safe
completed = output<number>(); // OutputEmitterRef<number>
deleted = output<number>(); // OutputEmitterRef<number>
onComplete() {
this.completed.emit(this.todoId());
}
onDelete() {
this.deleted.emit(this.todoId());
}
}
// Uso nel parent:
// <app-todo-item
// [text]="'Studiare Signals'"
// [todoId]="1"
// (completed)="handleComplete($event)"
// (deleted)="handleDelete($event)"
// />
モデル信号: 双方向バインディング
機能 model() をサポートする特別なシグナルを作成します。
双方向バインディング 親コンポーネントと子コンポーネントの間。それは現代の進化です
組み合わせの @Input() + @Output() 大会とともに
valueChange.
import { Component, model } from '@angular/core';
@Component({
selector: 'app-rating',
standalone: true,
template: `
<div class="stars">
@for (star of stars; track star) {
<span
(click)="value.set(star)"
[class.filled]="star <= value()">
★
</span>
}
</div>
`
})
export class RatingComponent {
// model() crea un WritableSignal con two-way binding
value = model(0); // ModelSignal<number>
stars = [1, 2, 3, 4, 5];
}
// Uso nel parent con banana-in-a-box syntax:
// <app-rating [(value)]="userRating" />
// Quando il child chiama value.set(3), userRating nel parent si aggiorna
model() の内部動作
model() 入力と出力の両方を自動的に作成します。
のために model 呼ばれた value、Angular は暗黙的に作成します
入力 value そして出力 valueChange。電話をかけるとき
value.set() o value.update()、コンポーネントの出力
イベントは自動的に valueChange、変更を親に伝播します。
シグナルクエリ: viewChild() および viewChildren()
デコレーターたち @ViewChild() e @ViewChildren() 彼らは今持っています
シグナルベースの対応物: viewChild() e viewChildren()。
これらの関数は、次のときに自動的に更新されるシグナルを返します。
参照される要素が変更されます。
import {
Component, viewChild, viewChildren,
ElementRef, AfterViewInit, effect
} from '@angular/core';
@Component({
selector: 'app-gallery',
standalone: true,
template: `
<input #searchInput type="text" placeholder="Cerca..." />
<div class="gallery">
@for (img of images(); track img.id) {
<img #galleryItem [src]="img.url" [alt]="img.title" />
}
</div>
`
})
export class GalleryComponent {
images = signal([
{ id: 1, url: '/img1.jpg', title: 'Foto 1' },
{ id: 2, url: '/img2.jpg', title: 'Foto 2' },
]);
// viewChild restituisce un Signal<ElementRef | undefined>
searchInput = viewChild<ElementRef>('searchInput');
// viewChild.required restituisce Signal<ElementRef> (no undefined)
// requiredInput = viewChild.required<ElementRef>('searchInput');
// viewChildren restituisce Signal<readonly ElementRef[]>
galleryItems = viewChildren<ElementRef>('galleryItem');
constructor() {
// Reagisci quando gli elementi della gallery cambiano
effect(() => {
const items = this.galleryItems();
console.log(`Gallery ha ${items.length} elementi`);
});
// Focus automatico sull'input di ricerca
effect(() => {
const input = this.searchInput();
if (input) {
input.nativeElement.focus();
}
});
}
}
シグナルクエリの利点
- AfterViewInit がありません: ライフサイクル フックは必要なくなりました。リファレンスが利用可能な場合、エフェクトは自動的に反応します。
- レスポンシブアップデート: 要素のリストが変更された場合 (例:
@for)、信号viewChildren更新します - タイプセーフティ:
viewChild.required()参照が存在することを保証し、チェックを排除しますundefined - 構成可能性: 使用できます
computed()クエリからデータを取得するには
生産のための実践的なパターン
個々の API を確認した後、日常的に使用する具体的なパターンを見てみましょう。 本番環境の Angular アプリケーション。
パターン 1: 信号によるフォームステータス
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-search',
standalone: true,
template: `
<input
[value]="query()"
(input)="onSearch($event)"
placeholder="Cerca prodotti..." />
<div class="filters">
<select (change)="onCategoryChange($event)">
<option value="">Tutte le categorie</option>
<option value="electronics">Elettronica</option>
<option value="books">Libri</option>
</select>
</div>
<p>Trovati {{ filteredProducts().length }} risultati</p>
@for (product of filteredProducts(); track product.id) {
<div class="product-card">
<h4>{{ product.name }}</h4>
<span>{{ product.price | currency:'EUR' }}</span>
</div>
}
`
})
export class SearchComponent {
// Stato reattivo del form
query = signal('');
category = signal('');
products = signal<Product[]>([]);
// Stato derivato: filtraggio reattivo
filteredProducts = computed(() => {
const q = this.query().toLowerCase();
const cat = this.category();
return this.products().filter(p =>
p.name.toLowerCase().includes(q) &&
(!cat || p.category === cat)
);
});
onSearch(event: Event) {
const value = (event.target as HTMLInputElement).value;
this.query.set(value);
}
onCategoryChange(event: Event) {
const value = (event.target as HTMLSelectElement).value;
this.category.set(value);
}
}
interface Product {
id: number;
name: string;
price: number;
category: string;
}
パターン 2: サービスと共有される状態
import { Injectable, signal, computed } from '@angular/core';
interface CartItem {
productId: number;
name: string;
price: number;
quantity: number;
}
@Injectable({ providedIn: 'root' })
export class CartService {
// Stato privato
private _items = signal<CartItem[]>([]);
private _discount = signal(0);
// Stato pubblico read-only
readonly items = this._items.asReadonly();
readonly discount = this._discount.asReadonly();
// Stato derivato
readonly itemCount = computed(() =>
this._items().reduce((sum, item) => sum + item.quantity, 0)
);
readonly subtotal = computed(() =>
this._items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
readonly total = computed(() => {
const sub = this.subtotal();
const disc = this._discount();
return sub - (sub * disc / 100);
});
readonly isEmpty = computed(() => this._items().length === 0);
// Azioni pubbliche
addItem(product: Omit<CartItem, 'quantity'>) {
this._items.update(items => {
const existing = items.find(i => i.productId === product.productId);
if (existing) {
return items.map(i =>
i.productId === product.productId
? { ...i, quantity: i.quantity + 1 }
: i
);
}
return [...items, { ...product, quantity: 1 }];
});
}
removeItem(productId: number) {
this._items.update(items =>
items.filter(i => i.productId !== productId)
);
}
applyDiscount(percent: number) {
this._discount.set(Math.min(percent, 50)); // Max 50%
}
clearCart() {
this._items.set([]);
this._discount.set(0);
}
}
このパターンがうまくいくから
このサービスは任意のコンポーネントに挿入できます。読み取るすべてのコンポーネント
cartService.total() テンプレート内の内容は自動的に更新されます
カートが変わるとき。手動サブスクリプションは必要ありません。
async pipe、それらは必要ありません Subject o BehaviorSubject。
それは純粋で反応性があり、カプセル化が保証されていました。 asReadonly().
パターン 3: API からデータをロードする
import { Component, signal, computed, inject, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
interface ApiState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
@Component({
selector: 'app-users-list',
standalone: true,
template: `
@if (state().loading) {
<div class="spinner">Caricamento...</div>
}
@if (state().error; as error) {
<div class="error">Errore: {{ error }}</div>
}
@if (state().data; as users) {
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }} - {{ user.email }}</li>
}
</ul>
}
`
})
export class UsersListComponent implements OnInit {
private http = inject(HttpClient);
state = signal<ApiState<User[]>>({
data: null,
loading: false,
error: null
});
// Computed di convenienza
isLoading = computed(() => this.state().loading);
hasError = computed(() => this.state().error !== null);
ngOnInit() {
this.loadUsers();
}
loadUsers() {
// Imposta loading
this.state.set({ data: null, loading: true, error: null });
this.http.get<User[]>('/api/users').subscribe({
next: (users) => {
this.state.set({ data: users, loading: false, error: null });
},
error: (err) => {
this.state.set({
data: null,
loading: false,
error: err.message || 'Errore sconosciuto'
});
}
});
}
}
interface User {
id: number;
name: string;
email: string;
}
RxJS との相互運用性: toSignal() および toObservable()
Angular は RxJS を放棄しません: 関数のおかげで 2 つの世界が共存し、連携します
パッケージ内の相互運用性の @angular/core/rxjs-interop。
これらの関数は、シグナルの命令型の世界と宣言型の世界の間の橋渡しとなります。
観測可能なものの。
toSignal(): Observable から Signal へ
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { interval } from 'rxjs';
@Component({
selector: 'app-dashboard',
standalone: true,
template: `
<p>Utenti: {{ users()?.length ?? 'Caricamento...' }}</p>
<p>Timer: {{ timer() }} secondi</p>
`
})
export class DashboardComponent {
private http = inject(HttpClient);
// Converti un HTTP Observable in Signal
// Il Signal sarà undefined finchè la risposta non arriva
users = toSignal(this.http.get<User[]>('/api/users'));
// Con valore iniziale (evita undefined)
timer = toSignal(interval(1000), { initialValue: 0 });
// Con requireSync per Observable che emettono subito (es. BehaviorSubject)
// config = toSignal(this.configService.config$, { requireSync: true });
}
toObservable(): シグナルからオブザーバブルへ
import { Component, signal, inject, effect } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-autocomplete',
standalone: true,
template: `
<input
[value]="searchTerm()"
(input)="onInput($event)"
placeholder="Cerca..." />
@for (result of results(); track result.id) {
<div>{{ result.name }}</div>
}
`
})
export class AutocompleteComponent {
private http = inject(HttpClient);
searchTerm = signal('');
// Converti Signal in Observable, applica operatori RxJS,
// poi riconverti in Signal
results = toSignal(
toObservable(this.searchTerm).pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term =>
term.length > 2
? this.http.get<SearchResult[]>(`/api/search?q=${term}`)
: []
)
),
{ initialValue: [] as SearchResult[] }
);
onInput(event: Event) {
const value = (event.target as HTMLInputElement).value;
this.searchTerm.set(value);
}
}
interface SearchResult {
id: number;
name: string;
}
toSignal() と toObservable() をいつ使用するか
| シナリオ | アメリカ合衆国 | 理由 |
|---|---|---|
| テンプレート内のHTTP | toSignal() |
置き換えます async パイプ、クリーナー |
| デバウンス/スロットル | toObservable() +RxJS |
RxJS オペレーターはタイム ストリームに最適です |
| UIのローカル状態 | signal() 直接 |
トグル、カウンター、フォームには RxJS は必要ありません |
| WebSocket/SSE | toSignal() |
ストリームをテンプレートの Signal に変換します |
| 複数の信号を組み合わせる | computed() |
よりも簡単 combineLatest |
Zone.js からの移行: 実践ガイド
既存のアプリケーションを Signals に移行する場合、全体を書き直す必要はありません。 1 つのアプローチを取ることができます 増分、コンポーネントの変換 一度に。ここでは実践的な戦略をご紹介します。
ステップ 1: 単純なプロパティをシグナルに変換する
// PRIMA (Zone.js)
@Component({ ... })
export class CounterComponent {
count = 0;
label = 'Contatore';
increment() {
this.count++;
}
}
// Template: {{ count }} - {{ label }}
// DOPO (Signals)
@Component({ ... })
export class CounterComponent {
count = signal(0);
label = signal('Contatore');
increment() {
this.count.update(v => v + 1);
}
}
// Template: {{ count() }} - {{ label() }}
ステップ 2: ゲッターを computed() に変換する
// PRIMA
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
get isValid(): boolean {
return this.email.includes('@') && this.name.length > 0;
}
// DOPO
fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
isValid = computed(() =>
this.email().includes('@') && this.name().length > 0
);
ステップ 3: サブスクリプションをエフェクトまたは toSignal() に変換する
// PRIMA (sottoscrizione manuale)
@Component({ ... })
export class UserProfileComponent implements OnInit, OnDestroy {
user: User | null = null;
private destroy$ = new Subject<void>();
constructor(private userService: UserService) {}
ngOnInit() {
this.userService.currentUser$
.pipe(takeUntil(this.destroy$))
.subscribe(user => this.user = user);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
// DOPO (con toSignal - zero boilerplate)
@Component({ ... })
export class UserProfileComponent {
private userService = inject(UserService);
// Si sottoscrive automaticamente, si pulisce automaticamente
user = toSignal(this.userService.currentUser$);
}
移行中に注意すること
- テンプレートを更新します。 Signal に変換される各プロパティには次のものが必要です
()テンプレート内:{{ count }}になる{{ count() }} - toSignal() には注入コンテキストが必要です。 コンストラクター内、またはクラス レベルで初期化されたフィールド内で呼び出す必要があります。
- 同じ成分を混ぜないでください。 Signal のようなメタ プロパティや単純な変数のようなメタを使用することは避けてください。コンポーネント全体を一度に移行することをお勧めします。
- テスト: 読み取るにはテストを更新する必要があります
component.count()の代わりにcomponent.count
ベストプラクティスとよくある間違い
数か月にわたる本番環境での使用を経て、Angular コミュニティは統合されました 一連のベストプラクティス。最も重要なものは次のとおりです。
信号の 8 つの黄金律
- アメリカ合衆国
signal()地元の州ごとに - トグル、カウンター、フォーム状態: 常に単純な変数よりもシグナルを優先します - アメリカ合衆国
computed()派生状態による - 計算可能な状態を重複させないでください。他のシグナルに依存する場合、それは計算されたものになります。 - アメリカ合衆国
effect()控えめに - 実際の副作用のみ (ロギング、localStorage、分析)。信号を相互に同期させるために使用しないでください。 - さらす
asReadonly()- サービスでは、内部シグナルをプライベートかつ書き込み可能にし、読み取り専用バージョンのみを公開します。 - あなたが好む
input()@Input() へ - 信号入力はより安全で、応答性が高く、タイプセーフです - アメリカ合衆国
computed()の代わりにeffect()状態を導出する - 結果が導出値の場合は、計算値を使用します。計算のためではなく、効果と行動のため - 内部の信号を変更しないでください
computed()- 関数は純粋でなければなりません。計算された内部のシグナルに書き込むと、予期しない動作が発生する - 再帰を避ける - エフェクト自体が読み込むシグナルに書き込むエフェクトにより、無限ループが作成される
避けるべきアンチパターン
// ERRORE 1: Modificare un Signal dentro computed()
const a = signal(1);
const b = signal(0);
const sum = computed(() => {
b.set(a() * 2); // MAI! Side effect in computed
return a() + b();
});
// ERRORE 2: Dimenticare le parentesi nel template
// <p>{{ count }}</p> <-- Mostra la funzione, non il valore!
// <p>{{ count() }}</p> <-- Corretto
// ERRORE 3: Mutare l'oggetto invece di crearne uno nuovo
const user = signal({ name: 'Fed', age: 30 });
// NON fare questo:
user().name = 'Marco'; // La mutation non viene rilevata!
// Fai invece:
user.update(u => ({ ...u, name: 'Marco' }));
// ERRORE 4: Loop infinito con effect
const counter = signal(0);
effect(() => {
// Legge e scrive lo stesso Signal = loop!
counter.set(counter() + 1);
});
// ERRORE 5: toSignal() fuori dall'injection context
@Component({ ... })
export class BadComponent {
// Questo funziona (campo di classe)
data = toSignal(this.http.get('/api/data'));
onClick() {
// Questo NON funziona (metodo, no injection context)
const result = toSignal(this.http.get('/api/other'));
}
}
シグナルを使用してはいけない場合
RxJS は、以下の場合に最適な選択肢です。
- 複雑なストリーム: WebSocket、サーバー送信イベント、再試行付きポーリング
- 時間演算子: デバウンス時間、スロットル時間、遅延、バッファー時間
- 条件付きフロー: 複雑なロジックを備えた switchMap、exhaustMap、mergeMap
- 競合状態: 以前のリクエストを削除する必要がある場合 (switchMap)
- 背圧: 高周波ストリームの管理
経験則: 問題が以下に関するものであれば、 時間 または 競争、RxJS を使用します。それが彼に関することなら stato、シグナルを使用します。
概要と次のステップ
この記事では、Angular のシグナル システムについて詳しく説明しました。 動機から構文まで、実践的なパターンから移行まで。重要なポイントは次のとおりです。
主要な概念
signal()で応答状態を作成します.set()e.update()computed()状態を遅延して取得し、記憶しますeffect()自動クリーンアップを使用して応答性の高い副作用を実行しますinput(),output()emodel()古典的なデコレータを置き換えますviewChild()eviewChildren()クエリをレスポンシブにするtoSignal()etoObservable()RxJS と Signals の世界を接続します。asReadonly()サービス内のカプセル化を保証します
シリーズの次の記事では、 ゾーンレスの変更検出、その方法を見てみましょう Zone.js をアプリケーションから完全に削除し、Signals を唯一のものとして活用します 反応性メカニズムとパフォーマンスの大幅な向上 そしてバンドルサイズで。







