Angularで効果的に状態を管理する方法
状態管理は、優雅にスケールするAngularアプリと保守の悪夢となるアプリの分かれ目です。しかし、ほとんどの開発者は根本的な疑問に悩んでいます:Signals、サービス、RxJS、NgRxのどれをいつ使うべきなのか?
この記事では、理論的なベストプラクティスではなく、実際のニーズに基づいて適切なAngular状態管理アプローチを選択するための実践的なフレームワークを提供します。Angular Signalsが輝く場面、RxJSを使ったサービスが適している場面、そしてNgRxやSignalStoreが必要になる場面を探ります。
重要なポイント
- Angularアプリケーションには3つの状態レイヤーがあります:ローカルコンポーネント、共有フィーチャー、グローバルアプリケーション状態
- Signalsは、Angular 16以降、同期的でリアクティブな状態に対する最もシンプルなソリューションを提供します
- RxJSを使ったサービスは、フレームワークのオーバーヘッドなしで非同期操作と中程度の複雑性を処理します
- NgRxとSignalStoreは、複雑な状態の相互作用に構造化されたパターンが必要な場合に価値を発揮します
Angularアプリケーションにおける状態のスコープを理解する
ツールを選択する前に、状態のスコープを理解しましょう。Angularアプリケーションには通常、3つの異なる状態レイヤーがあり、それぞれ異なる管理戦略が必要です。
ローカルコンポーネント状態
ローカル状態は単一のコンポーネントと共に生まれ、消滅します。フォーム入力、UIトグル、一時的な計算などが該当します。これらのシナリオでは、Angularコンポーネント状態のベストプラクティスはシンプルさを重視します:リアクティブな状態にはSignalsを、静的な値には通常のクラスプロパティを使用します。
@Component({
selector: 'app-product-card',
template: `
<div [class.expanded]="isExpanded()">
<button (click)="toggle()">{{ isExpanded() ? 'Less' : 'More' }}</button>
</div>
`
})
export class ProductCardComponent {
isExpanded = signal(false);
toggle() {
this.isExpanded.update(v => !v);
}
}
共有フィーチャー状態
フィーチャー内の複数のコンポーネントが同じデータを必要とする場合、サービスが調整ポイントになります。ヘッダー、サイドバー、チェックアウトページに表示されるショッピングカートは、一元管理が必要ですが、必ずしもグローバルストアは必要ありません。
グローバルアプリケーション状態
認証ステータス、ユーザー設定、アプリ全体の通知は、アプリケーション全体に影響します。ここでAngularでグローバル状態を管理するには、複雑性と保守性を慎重に検討する必要があります。
Angular Signals:リアクティブ状態のモダンなデフォルト
Angular 16以降、Signalsはobservableの複雑さなしに細粒度のリアクティビティを提供します。同期的な状態更新と計算値に優れています。
Signalsが優れている場面
以下のようなリアクティブな状態が必要な場合にSignalsを使用します:
- 同期的に更新される
- 計算値を効率的に導出する
- Angularの変更検知とシームレスに統合される
@Injectable({ providedIn: 'root' })
export class CartService {
private items = signal<CartItem[]>([]);
// 計算されたsignalは自動的に更新されます
total = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
addItem(item: CartItem) {
this.items.update(items => [...items, item]);
}
}
主な利点は?Signalsは、サブスクリプション管理やメモリリークといったRxJSの一般的な落とし穴を排除しながら、粒度の細かい更新によってより優れたパフォーマンスを提供します。
Discover how at OpenReplay.com.
RxJSを使用したサービスベースの状態管理
非同期操作や複雑なデータストリームには、RxJS observableが依然として非常に有用です。Angular SignalsとNgRxの議論では、この中間地点を見落としがちです:RxJSを使ったサービスは、フレームワークのオーバーヘッドなしで多くの実世界のシナリオを処理します。
SignalsとRxJSの組み合わせ
モダンなAngularアプリケーションは、ハイブリッドアプローチから恩恵を受けることが多いです:
@Injectable({ providedIn: 'root' })
export class UserService {
private currentUser = signal<User | null>(null);
// 読み取り専用signalとして公開
user = this.currentUser.asReadonly();
constructor(private http: HttpClient) {}
loadUser(id: string) {
return this.http.get<User>(`/api/users/${id}`).pipe(
tap(user => this.currentUser.set(user)),
catchError(error => {
this.currentUser.set(null);
return throwError(() => error);
})
);
}
}
このパターンは、状態の保存にはSignalのシンプルさを、非同期操作にはRxJSのパワーを提供します。
NgRxとSignalStore:複雑性が構造を要求する場合
NgRxは、真に複雑な状態要件がある場合に意味を持ちます:マルチユーザーコラボレーション、楽観的更新、タイムトラベルデバッグ、または広範なクロスフィーチャー調整などです。
NgRx SignalStore:軽量な代替手段
NgRx SignalStoreとSignalsを組み合わせることで、ボイラープレートなしで構造を提供します。SignalStoreは、AngularのネイティブなSignalパフォーマンスを活用しながら、状態管理パターンを提供します:
export const CartStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withComputed((state) => ({
itemCount: computed(() => state.items().length),
isEmpty: computed(() => state.items().length === 0)
})),
withMethods((store) => ({
addItem: (item: CartItem) =>
patchState(store, { items: [...store.items(), item] }),
clear: () =>
patchState(store, { items: [] })
}))
);
SignalStoreは、サービス以上の構造が必要だが、従来のNgRxほどの儀式は不要なアプリケーションにとって最適な選択肢です。
正しい選択をする:実践的な意思決定フレームワーク
「どの状態管理が最適か?」と問うのをやめて、「この特定の状態には何が必要か?」と問い始めましょう。
Signalsを直接使用する場合:
- 状態が同期的でローカルである
- 計算値が必要である
- 頻繁な更新でパフォーマンスが重要である
SignalsまたはRxJSを使ったサービスを使用する場合:
- 複数のコンポーネントが状態を共有する
- 非同期操作を処理している
- 状態ロジックがフィーチャー固有である
NgRx SignalStoreを検討する場合:
- チーム全体で一貫したパターンが必要である
- 状態の相互作用が複雑になる
- 完全なReduxなしで構造が欲しい
従来のNgRxを採用する場合:
- 厳格な要件を持つエンタープライズアプリケーションを構築している
- 複数の開発者が予測可能なパターンを必要としている
- DevTools、タイムトラベルデバッグ、またはエフェクトが必要である
避けるべき一般的な落とし穴
Angular状態管理における最大の間違いは、間違ったツールを選ぶことではなく、過剰設計することです。すべてのカウンターをNgRxに入れないでください。コンポーネント専用の状態にサービスを作成しないでください。Signalsで十分な場合にRxJSを使用しないでください。
シンプルに始めましょう。コンポーネントが状態を共有する場合にサービスに抽出します。サービスが扱いにくくなったらストアを導入します。この段階的なアプローチは、過小設計と過剰設計の両方を防ぎます。
結論
Angularにおける効果的な状態管理は、すべてに対して1つのアプローチを選択することではありません。モダンなAngularアプリケーションは、階層化された戦略で成功します:ローカルなリアクティビティにはSignals、共有ロジックにはサービス、複雑な調整にはストアを使用します。
Signalsをデフォルトとして始めましょう。共有が必要になったらサービスに移行します。NgRx(従来型またはSignalStore)は、真に複雑な状態の課題のために取っておきます。この実用的なアプローチは、早すぎる最適化を避けながら、コードベースの保守性を保ちます。
最良の状態管理戦略は、実際の問題を解決する最もシンプルなものです。
よくある質問
はい、SignalsとRxJSを組み合わせることは推奨されるパターンです。状態の保存にはSignalsを、HTTPリクエストのような非同期操作の処理にはRxJSを使用します。このハイブリッドアプローチは、両方の長所を提供します。
複数のフィーチャーにまたがる複雑な状態の相互作用がある場合、タイムトラベルデバッグが必要な場合、またはチームの一貫性のために厳格な状態更新パターンが必要な場合にNgRxを検討してください。サービスの保守が困難になってきたら、それがNgRxを評価する合図です。
いいえ、SignalsはRxJSを置き換えるのではなく補完します。Signalsは同期的な状態管理に優れていますが、RxJSはストリーム、非同期操作、複雑なイベント調整の処理に不可欠です。ほとんどのアプリケーションは両方を使用することで恩恵を受けます。
Signalsは粒度の細かいリアクティビティを提供し、変更された値に依存するコンポーネントのみを更新します。このターゲットを絞ったアプローチは、頻繁な状態更新や大きなコンポーネントツリーを持つアプリケーションにおいて、ゾーンベースの変更検知を大幅に上回るパフォーマンスを発揮します。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.