インフラジスティックス・ジャパン株式会社Blog

インフラジスティックス・ジャパン株式会社のチームメンバーが技術トレンド、製品Tips、サポート情報からライセンス、日々の業務から感じることなど、さまざまなトピックについてお伝えするBlogです。

『 Angular 状態管理 』プロジェクトを進めるためのベストプラクティス!

大量のデータ通信を伴う Angular アプリを構築する際には、データ効率、ネットワーク遅延、スケーラビリティ、リソース管理、テスト、ユーザーエクスペリエンス(UX) などの要因に対処するための包括的なアプローチが必要です。また、アプリのスケーラビリティと一貫性を保ちながらデータの競合を回避するため、効果的な Angular の状態管理は非常に重要です。適切な状態管理がなければ、データはアプリ内のどこにでも散在してしまう可能性があります。

では、Angular の状態管理とは何でしょうか?また、どのように実装すべきでしょうか?この記事では、『 Ignite UI for Angular 』 を用いた Angular 状態管理のベストプラクティスを紹介します。RxJS(Reactive Extensions for JavaScript) を利用したサービスベースの実装方法や、NgRx(Angular Reactive Extensions) を使用した状態管理アプローチを、実際のコード例を交えて詳しく解説します。

Ignite UI for Angular を試してみましょう

Angular の状態管理とは

Angular の状態管理とは、Angular アプリケーションの状態を処理および維持するためのさまざまな手法と戦略を含むプロセスです。ここで言う状態とは、外部サービスから取得され、アプリによって操作および表示されるデータ、および特定のユーザーに適した UI を構築するためにアプリケーションが作成、保存、および使用するすべてのデータを指します。

簡単に言うと、なぜ Angular で状態管理が必要なのでしょうか?それは、新しいページや新しい機能、現在の状態(例えばユーザーがログインしているかどうか)を示すアプリの新しい UI を追加するたびに、アプリの複雑さが増すからです。単純なアプリの範囲を超えると、これらすべてを管理するのは非常に困難になります。効果的な状態管理方法を適用しないと、データが散在し、アプリのメンテナンスが難しくなります。

Angular で状態管理を行う最も一般的な方法は、データを取得、共有、更新するだけでなく、クリーンで適切に整理された方法でデータにアクセスし、操作し、変更できるようにする状態コンテナーを使用することです。これにより、アプリケーションのスケーラビリティと保守性が向上します。

判断を下すこと、またはその必要性を知ること

アプリケーションの規模が拡大し複雑になるにつれて、大量のデータを処理することが一般的になります。大規模なプロジェクトでのデータ管理は難しい場合があります。特に非同期データの処理やデータ変更の追跡、大量のコードを扱う際には、問題がさらに複雑化します。

絶え間ない計算、リフロー、APIリクエストなどの舞台裏での処理が増えると、ブラウザのメインスレッドがブロックされ、アプリが応答しなくなる可能性があります。こうした状態を防ぐためには、状態管理を体系的にアプローチすることが重要です。さもなければ、バグや技術的負債の修正に多大なコストがかかることになります。

では、Angular状態管理(State Management Angular) はいつ使用するのでしょうか。ここでは、いくつかのユースケースを紹介します。

  • アプリケーションの複雑化

データフローが最小限の単純なアプリでは、状態管理は必要ないかもしれません。しかし、複数のコンポーネント、多くのデータ操作、増え続ける機能セット、複雑なユーザーインターフェイスを含む大規模なソフトウェアプロジェクトでは、状態管理が特に有益です。

ここで達成できることは、秩序の維持、データの一貫性、パフォーマンスの向上、UXの向上+レスポンシブなインターフェースの提供です。

  • データの共有

状態管理は、コンポーネントやサービスなど、アプリケーションのさまざまな部分間でデータを共有する必要がある場合に不可欠になります。

ここで実現されるのは、データの一元管理とデータ共有の簡素化です。

  • 非同期操作の場合

APIからのデータの取得、リアルタイム更新の処理、ユーザー操作の処理などの非同期操作がアプリに含まれる場合、状態管理が役立ちます。

ここで達成されるのは、合理化された非同期データフローとレスポンシブなUIです。

  • 予測可能な状態変化を目指す場合

Angularアプリケーションで状態がいつどのように変化するかを厳密に制御したい場合、状態管理ライブラリを使用することで、このプロセスを容易にすることができます。

ここで達成されるのは、予測可能でデバッグが容易なコードベースの確保です。

開発者の作業を容易にする『 Ignite UI 』を使用した Angular 状態管理のベストプラクティス

Angular で状態を管理する方法には、アプリの複雑さ、プロジェクトの要件、開発者の知識や好みに応じて、さまざまな手法とツールがあります。例えば、状態が一つのコンポーネントに固有の場合、コンポーネント自体内で Angular の状態を管理できます。この場合、データを格納および更新するには、クラスのプロパティと変数に依存します。

一方、大規模なアプリケーションでは、状態管理ライブラリの使用を検討することをお勧めします。状態管理ライブラリを使用すると、複雑な状態の管理が効率的に行え、コードのメンテナンス性が向上します。

それでは、最高の Angular 状態管理手法を詳細に見ていきましょう。

状態を管理するための優れた方法として『 RxJS 』を利用したサービスの基本実装

サービスとRxJSを使用したAngularでの状態管理は、強力で柔軟なアプローチです。以下にいくつかのベストプラクティスを示します。

  • 単一責任の原則

各サービスに一つの責任を持たせることが重要です。特定のタスク(例:ユーザーのログインと認証、ユーザーデータの管理など)を処理するようにサービスを設計することで、コードベースをクリーンで保守しやすくすることができます。各サービスは、デバッグとテストを容易にするために、状態の特定の部分を管理する必要があります。

  • 信頼できる唯一の情報源としてのサービス

サービスを信頼できる唯一の情報源として扱います。コンポーネントは、状態情報とその更新をサービスに依存し、一方向のデータフローを促進します。これにより、アプリケーションの予測可能性が向上し、推論しやすくなります。

  • サービスで状態ロジックをカプセル化する

状態ロジックはコンポーネント内ではなく、サービス内に保持します。サービスはビジネスロジックと状態管理を処理し、コンポーネントはプレゼンテーションとユーザーインターフェイスに重点を置きます。この関心事の分離により、テストの容易さと再利用性が向上します。

  • エラー処理

サービス内でエラー処理を実装します。Observables(オブジェクト)のエラーをキャッチして処理し、スムーズなユーザーエクスペリエンスを提供するとともに、アプリケーションのクラッシュを防ぎます。

  • RxJSのオペレーターとサブジェクトを利用した状態管理

まず、適切なサブジェクト(基本的なマルチキャストオブジェクト)を選びます。これにより、状態を保持せずに複数のサブスクライバーに値を出力できます。イベントやアクションの処理に適しています。次に、BehaviorSubject(オブジェクト)を定義します。これは初期値を必要とし、その現在の値を新しいサブスクライバーに出力するサブジェクトの一種です。状態を初期化し、新しいサブスクライバーに常に最新の状態を提供するため、状態管理に役立ちます。ReplaySubject(オブジェクト)は、指定された数の以前の値を新しいサブスクライバーに出力します。過去の状態値を再生する必要がある場合に便利です。最後に、AsyncSubject(オブジェクト)は完了時に最後の値(最後の値のみ)を出力します。これは、最終的に出力された値のみが必要なシナリオで役立ちます。

次に、複数のステートストリームを使用します。「combineLatest」や「withLatestFrom」などのRxJSオペレーターを使用して、複数のステートストリームを結合します。多くの場合、複数のオブザーバブルに基づいて状態を導出する必要があります。combineLatestのようなオペレーターを使用すると、他のステートストリームの組み合わせに基づいて新しいステートオブザーバブルを作成できます。

次に、サービスとその状態管理ロジックの単体テストを記述します。テストを通じてサービスが期待通りに動作することを確認することは、アプリケーションの安定性を維持し、問題を早期に発見するために重要です。

例えば、サービスを使用して気象データを取得し保存するロジックを一元化できます。これにより、気象データを必要とするすべてのコンポーネントが、信頼できる唯一の情報源からデータを取得できるようになります。

『 WeatherService(気象サービス)』

interface WeatherData {
    temperature: number;
   description: string;
}
@Injectable({
  providedIn: 'root'
})
export class WeatherService {
  private apiKey = 'YOUR_API_KEY';
  private _weather: BehaviorSubject<WeatherData | null> = new BehaviorSubject<WeatherData | null>(null);
  public weather$: Observable<WeatherData | null> = this._weather.a
constructor(private http: HttpClient) { }
  private getApiUrl(location: string): string {
   return `https://api.weatherapi.com/v1/current.json?key=${this.apiKey}&q=${location}`;
  }
  fetchWeather(location: string): Observable<WeatherData | null> {
    const apiUrl = this.getApiUrl(location);
    return this.http.get<WeatherData>(apiUrl).pipe(
      tap((data: WeatherData) => this._weather.next(data)),
      catchError(error => {
        console.error('Error fetching weather data', error);
        this._weather.next(null);
        return of(null);
      })
    );
  }
}

「WeatherService」は、APIからデータを取得し、それを「BehaviorSubject」に格納します。これにより、サービスを注入したすべてのコンポーネントからデータにアクセスできるようになります。RxJSの「BehaviorSubject」を使用することで、アプリケーション内でデータの変更をリアクティブに管理できます。

サービスで気象データが更新されると、「BehaviorSubject」をサブスクライブしているすべてのコンポーネントが自動的に通知され、その結果ビューが更新されます。これにより、最新のデータが常に表示され、ユーザーエクスペリエンスが向上します。

『 Component TS file 』

export class WeatherDisplayComponent implements OnInit {
      weather$: Observable<WeatherData | null>;
      constructor(private weatherService: WeatherService) {
       this.weather$ = this.weatherService.weather$;
      }
     ngOnInit(): void {
      this.weatherService.fetchWeather('New York');
      }
   

『 Component HTML file 』

<div *ngIf="weather$ | async as weather"> 
   <h2>Current Weather</h2> 
   <p>Temperature: {{ weather.temperature }}°C</p> 
   <p>Description: {{ weather.description }}</p> 
</div> 

コンポーネントは、データの取得や状態管理について心配することなく、データの表示とユーザー操作の処理に集中できます。このように関心事を分離することで、コンポーネントの記述、テスト、保守が容易になります。

次に、NgRxを使用した状態管理の例を示します。

Angularアプリケーションの状態管理にNgRxを使用すると、特に大規模で複雑なアプリケーションに対して、追加の利点を提供できます。NgRxは、Reduxパターンを使用してAngularアプリケーションのリアクティブ状態を管理するためのライブラリです。

まず、気象データの構造を定義します。

『 weather.model.ts 』

export interface WeatherData {
       temperature: number;
       description: string;
    }
    export interface WeatherState {
       weather: WeatherData | null; 
       loading: boolean; 
       error: string | null; 
    

次に、天気予報機能でさまざまなイベントを表すアクションを定義します。

『 weather.actions.ts 』

export const loadWeather = createAction(
      '[Weather] Load Weather',
      props<{ location: string }>()
    );
    export const loadWeatherSuccess = createAction(
     '[Weather] Load Weather Success',
      props<{ weather: WeatherData }>()
    );
    export const loadWeatherFailure = createAction(
      '[Weather] Load Weather Failure', 
      props<{ error: string }>()
    ); 

次に、アクションに基づいて状態の変化を処理するためのレデューサーを定義します。

『 weather.reducer.ts 』

export const initialState: WeatherState = {
      weather: null,
      loading: false,
      error: null,
    };
    export const weatherReducer = createReducer(
      initialState,
      on(loadWeather, (state) => ({
       ...state,
       loading: true,
       error: null,
      })),
      on(loadWeatherSuccess, (state, { weather }) => ({
       ...state,
       weather,
       loading: false,
      })),
     on(loadWeatherFailure, (state, { error }) => ({
       ...state,
       loading: false,
       error,
      }))
    );

次に、API呼び出しなどの副作用を処理するためのエフェクトを定義します。

『 weather.effects.ts 』

@Injectable()
export class WeatherEffects {
  loadWeather$ = createEffect(() =>
    this.actions$.pipe(
      ofType(loadWeather),
      mergeMap(action => this.http.get<WeatherData>(`https://api.weatherapi.com/v1/current.json?key=YOUR_API_KEY&q=${action.location}`).pipe(
        map(weather => loadWeatherSuccess({ weather })),
        catchError(error => of(loadWeatherFailure({ error: error.message })))
      )
      ))
  );
  constructor(private actions$: Actions, private http: HttpClient) { }
}

『 app.module.ts 』で、NgRx ストアとエフェクトを登録して、アプリケーション全体で使用できるようにします。

@NgModule({
       imports: [
        BrowserModule,
        HttpClientModule,
        StoreModule.forRoot({ weather: weatherReducer }),
        EffectsModule.forRoot([WeatherEffects]),
       ]
    }

気象機能の状態から特定の部分を取り出すための気象状態のセレクターを定義します。

『weather.selectors.ts』

export const selectWeatherState = createFeatureSelector<WeatherState>('weather');
export const selectWeather = createSelector(
  selectWeatherState,
  (state: WeatherState) => state.weather
);
export const selectLoading = createSelector(
  selectWeatherState,
  (state: WeatherState) => state.loading
);
export const selectError = createSelector(
  selectWeatherState,
  (state: WeatherState) => state.error
);

ストアをコンポーネントに注入し、アクションをディスパッチして気象データを読み込み、表示するための状態を選択します。

『 weather-display.component.ts 』

export class WeatherDisplayComponent implements OnInit {
      weather$: Observable<WeatherData | null>;
      loading$: Observable<boolean>;
      error$: Observable<string | null>;
      constructor(private store: Store<{ weather: WeatherState }>) { 
       this.weather$ = this.store.pipe(select(selectWeather)); 
       this.loading$ = this.store.pipe(select(selectLoading)); 
       this.error$ = this.store.pipe(select(selectError)); 
      }
      ngOnInit(): void { 
       this.store.dispatch(loadWeather({ location: 'New York' })); 
      }
    }

『 Component HTML file 』

<div *ngIf="loading$ | async">Loading...</div>
<div *ngIf="error$ | async as error">{{ error }}</div>
<div *ngIf="weather$ | async as weather">
   <h2>Current Weather</h2>
   <p>Temperature: {{ weather.temperature }}°C</p>
   <p>Description: {{ weather.description }}</p>
</div>

状態管理ライブラリの使用を検討する 『 NgRx 』

NgRxは、リアクティブプログラミングの原則を使用してAngularアプリケーションの状態を管理するための強力なライブラリです。Reduxパターンに触発され、Angularと緊密に統合されているため、アプリケーションの状態の保守とデバッグが容易になります。

物事をより明確にするために、ここではNgRxのコアコンセプトについて説明します。

ストア( Store ) : 一貫性のある状態管理を保証し、一方向のデータフローを促進し、アプリケーションの状態を処理するための構造化された方法を提供します。これは、アプリケーションの状態全体を不変のオブジェクトツリーとして表します。このツリーの各ノードは、オブザーバブルを介してアクセスされるアプリケーション状態の特定の部分に対応しており、コンポーネントが状態の特定のスライスをサブスクライブできるようにします。

アクション( Actions ) : NgRxを利用したアプリケーションのイベントを記述します。これらは、アプリケーション内で発生した事象を表し、事実の記述と見なすことができます。

レデューサー( Reducers ) : 現在の状態とアクションを受け取り、新しい状態を返す純粋関数です。レデューサーは、アクションに応じて状態がどのように変化するかを指定します。

セレクタ( Selectors ) : ストアから状態のスライスを選択するために使用される関数です。これらは状態構造のカプセル化に役立ち、より複雑なセレクタを作成するために組み合わせて使用できます。

エフェクト( Effects ) : 副作用はストアの外で処理されます。エフェクトは、特定のアクションをリッスンし、API呼び出しなどのタスクを実行し、その結果に基づいて新しいアクションをディスパッチするクラスです。Angularの依存性注入を使用し、RxJSオブザーバブルを活用して副作用を管理します。

『 NgRx 』を使用する際の簡単なヒントとコツ

まず、状態を整理し、特徴状態の分離を実行します。これを行うには、機能モジュールごとに状態を分離します。各機能モジュールには、モジュール化と保守性を維持するために、独自の状態スライスが必要です。各機能モジュール内で、アクション、レデューサー、セレクター、エフェクトなど、一貫したフォルダー構造を使用します。

その後、明確なアクションタイプを定義できます。アクションの命名については、説明的なアクションタイプを使用します。アクションタイプには、それらが属する機能の前に付けます(例:「[Weather] Load Weather」)。Action Payloads には、厳密に型指定されたペイロードを使用します。 また、ペイロードのインターフェースを定義して、タイプ セーフを確保する必要があります。 NgRxに関してもう一つ重要なことは、純粋なレデューサーを書くことです。 レデューサーが純粋な関数であることを常に確認し、既存の状態を変更するのではなく、常に新しい状態オブジェクトを返します。また、レデューサーのロジックは最小限に抑えます。レデューサーの副作用や複雑なロジックを避けてください。それらを効果に委任します。

ステートアクセスにセレクターを使用することも考慮すべき点です。 セレクターを使用して、状態構造をカプセル化できます。これにより、コンポーネント コードをクリーンに保ち、状態の形状を抽象化できます。または、セレクターを作成して、より複雑な状態選択を作成することもできます。重要なことは、エフェクトを使用して API 呼び出し、ログ記録などの副作用を処理することです。これにより、減速機の純度が保たれ、コンポーネントが清潔に保たれます。また、エフェクトのエラーは常に処理する必要があることを覚えておいてください。適切なアクションを使用して、エラー状態を管理します。

最後に、テストに関して注意すべき点がいくつかあります。 レデューサーの単体テストを記述して、特定のアクションに対して正しい状態を返すことを確認します。エフェクトをテストして、正しいアクションをディスパッチし、副作用を適切に処理することを確認します。 「MockStore」を使用して、実際のNgRxストアから分離してコンポーネントをテストすることをお勧めします。

これをAngular 状態管理(Angular State Management)の例でより鮮明に示してみましょう。

天気予報アプリを作成していると想像してください。その中で、APIから天気予報に関するデータを取得し、それをアプリケーションのさまざまな部分に表示する必要があります。これを行うには、APIリクエストを処理し、指定されたデータを保存するWeatherServiceをAngularで作成できます。このサービスをさまざまなコンポーネントに注入して、天気情報にアクセスして表示できます。

結論

結論として、『 RxJS 』および『 NgRx 』でサービスを使用した Angular アプリケーションのこれらの状態管理手法は、プロジェクトの要件とチームの専門知識に基づいて慎重に検討する必要がある独自の利点とトレードオフを提供します。最終的には、プロジェクトの特定の要件、開発の制約、チームの能力を優先して決定する必要があります。これらの要素を慎重に評価することで、開発者は目標に最も適した状態管理アプローチを選択し、Angular アプリケーションを正常に提供できます。

画像説明

この記事の原文は以下よりご確認いただけます。 [Katie Mikova(https://www.infragistics.com/community/blogs/b/infragistics/posts/angular-state-management?gasource=login.microsoftonline.com&gamedium=referral&gacampaign=(not%20set)&gaterm=&gagclid=#angular-state-management-best-practices) (ケイティ・ミコヴァ) / 2024年6月13日(木)