Back

Angularで効果的に状態を管理する方法

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の一般的な落とし穴を排除しながら、粒度の細かい更新によってより優れたパフォーマンスを提供します。

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.

OpenReplay