背景
Angular アプリを NgRx を使って作ったが、 NgRx よりも NGXS の方がシンプルに書けるようなので、置き換えてみることにした。
会社でも NGXS を使ってるが、まだ使い方など腑に落ちてないので、NGXS の学習も含めて取り組んでみる。
参考記事
以下の記事を参考に、置き換えに取り組む。
やってみる
NGXS のインストール
npm install @ngxs/store --save
ついでに、 Angular のバージョンを 12 -> 13 にバージョンアップする。
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
を導入する。
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()
で初期化する。
...
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。
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 というものがあるらしいので、こちらをアプリに導入していこうと思う。