LoginSignup
1
0

More than 1 year has passed since last update.

Angular アプリで NgRx を NGXS に置き換えてみた

Posted at

背景

Angular アプリを NgRx を使って作ったが、 NgRx よりも NGXS の方がシンプルに書けるようなので、置き換えてみることにした。
会社でも NGXS を使ってるが、まだ使い方など腑に落ちてないので、NGXS の学習も含めて取り組んでみる。

参考記事

以下の記事を参考に、置き換えに取り組む。

やってみる

NGXS のインストール

bash
npm install @ngxs/store --save

ついでに、 Angular のバージョンを 12 -> 13 にバージョンアップする。

bash
npx @angular/cli@13 update @angular/core@13 @angular/cli@13
npx @angular/cli@13 update @angular/material@13

ng serve でエラーが出たので、 compilerOptions に skipLibCheck: true を入れておく。

Action を定義する

~~~.actions.ts に定義していた Action とほぼ同じ。
Reducer が不要となるため、

export type TrainingActions = SetAvailableTrainings | SetFinishedTrainings | StartTraining | StopTraining;

のようなコードは削除しておく。

また、import を簡単にするため、 namespace を導入する。

src/app/training/training.actions.ts
import { Exercise } from './exercise.model';

export const SET_AVAILABLE_TRAININGS = '[Training] Set Available Trainings';
export const SET_FINISHED_TRAININGS  = '[Training] Set Finished Trainings';
export const START_TRAINING          = '[Training] Start Training';
export const STOP_TRAINING           = '[Training] Stop Training';

export namespace TrainingAction {
  export class SetAvailable {
    static readonly type = SET_AVAILABLE_TRAININGS;

    constructor(public payload: Exercise[]) {}
  }

  export class SetFinished {
    static readonly type = SET_FINISHED_TRAININGS;

    constructor(public payload: Exercise[]) {}
  }

  export class Start {
    static readonly type = START_TRAINING;

    constructor(public payload: string) {}
  }

  export class Stop {
    static readonly type = STOP_TRAINING;
  }
}

State を定義する

状態を表す State を定義する。
State クラスを記述するため、 training.state.ts を作成する。

Store モジュールを import する

ルートモジュールに Store モジュールを import する。
Store で管理する State を配列として渡し、 forRoot() で初期化する。

src/app/app.module.ts
...
import { NgxsModule }              from '@ngxs/store';

...
import { TrainingState }        from './training/training.state';

@NgModule({
  declarations: [
    ...
  ],
  imports: [
    ...
    NgxsModule.forRoot([TrainingState])
  ],
  providers: [
    ...
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

コンポーネントから Action をディスパッチする

training.service.ts のロジックを修正する。
@ngxs/store を inport し、各メソッドで Action をディスパッチする。
また、 Store からデータを取得するため、 @Select デコレータをつけたプロパティを定義しておく。
Store から提供される値は Observable。

src/app/training/training.service.ts
import { Injectable }       from '@angular/core';
import { AngularFirestore } from '@angular/fire/compat/firestore';
import { Select, Store }    from '@ngxs/store';

import { Observable, Subscription } from 'rxjs';
import { take }                  from 'rxjs/operators';
import 'rxjs/add/operator/map';

import { Exercise }      from './exercise.model';
import { UIService }     from '../shared/ui.service';
import * as UI           from '../shared/ui.actions';
import { TrainingAction} from './training.actions';
import { TrainingState } from './training.state';

@Injectable()
export class TrainingService {
  @Select(TrainingState.getActiveTraining) activeTraining$: Observable<Exercise>;

  private fbSubs:             Subscription[] = [];

  constructor(
    private db:        AngularFirestore,
    private uiService: UIService,
    private store:     Store
  ) {}

  fetchAvailableExercises() {
    this.store.dispatch(new UI.StartLoading());
    this.fbSubs.push(this.db
    .collection('availableExercises')
    .snapshotChanges()
    .map(docArray => {
      // throw(new Error());
      return docArray.map(doc => {
        return {
          id: doc.payload.doc.id,
          ...doc.payload.doc.data() as Exercise
        };
      });
    })
    .subscribe((exercises: Exercise[]) => {
      this.store.dispatch(new UI.StopLoading());
      this.store.dispatch(new TrainingAction.SetAvailable(exercises));
    }, error => {
      this.store.dispatch(new UI.StopLoading());
      this.uiService.showSnackbar(
        'Fetching exercises failed, please try again later',
        null,
        3000
      );
    }));
  }

  startExercise(selectedId: string) {
    this.store.dispatch(new TrainingAction.Start(selectedId));
  }

  completeExercise() {
    this.activeTraining$.pipe(take(1)).subscribe(ex => {
      this.addDataToDatabase({
        ...ex,
        date:  new Date(),
        state: 'completed'
      });
      this.store.dispatch(new TrainingAction.Stop());
    });
  }

  cancelExercise(progress: number) {
    this.activeTraining$.pipe(take(1)).subscribe(ex => {
      this.addDataToDatabase({
        ...ex,
        duration: ex.duration * (progress / 100),
        calories: ex.calories * (progress / 100),
        date:     new Date(),
        state:    'cancelled'
      });
      this.store.dispatch(new TrainingAction.Stop());
    });
  }

  fetchCompleteOrCancelledExercises() {
    this.fbSubs.push(this.db
      .collection('finishedExercises')
      .valueChanges()
      .subscribe((exercises: Exercise[]) => {
        this.store.dispatch(new TrainingAction.SetFinished(exercises));
      }));
  }

  cancelSubscriptions() {
    this.fbSubs.forEach(sub => sub.unsubscribe());
  }

  private addDataToDatabase(exercise: Exercise) {
    this.db.collection('finishedExercises').add(exercise);
  }
}

その他、各コンポーネントでも @Select で値を切り取っていく。

やってみた感想

一度 NgRx で組んでいれば、置き換えは思ったよりも楽だった。
途中エラーにも遭遇したが、 static キーワードが抜けていたためだったようだ。

また、NGXS の方がスッキリ書ける印象。
NgRx は select を使う際に selector をロジック内で書き込む必要があるが、 NGXS はそれをロジック外に書けるので、ロジック部分の見た目がスッキリできる。
(書いてみたら実感できると思う)

コード量については、あまり変わらないと思う。
NgRx の reducer.ts が、そのまま NGXS の state.ts に移せる感じだ。

今後の学習方針

Action 定義を省略する方法として、 ngxs-labs/emitter というものがあるらしいので、こちらをアプリに導入していこうと思う。

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0