Article Outline
NgRxってなに
状態管理ライブラリの一種でAngular版のReduxです。 Rxとついている通りRxJsを使って状態にアクセスします。
なんで必要なの
Angularはサービスがシングルトンなのでサービス内に状態を定義できるが、状態が多くなると管理が複雑になりバグの原因になる。 対策として状態管理のベストプラクティスであるFluxアーキテクチャを適用するためにNgRxを用いる。
なにがいるの
# 状態管理
yarn add @ngrx/store
# developer-toolsでのデバッグができるようになる
yarn add -D @ngrx/store-devtools
# 副作用の管理
yarn add @ngrx/effects
構成
大体こんな感じ。
.
├── counter.component.ts
├── counter.module.ts
└── store
├── counter.actions.ts
├── counter.effect.ts
├── counter.reducer.ts
└── counter.selector.ts
コードを書く
1. actionを定義
import { createAction, props } from '@ngrx/store';
export const increment = createAction('[Counter Component] increment');
export const decrement = createAction('[Counter Component] decrement');
# propsを受け取るaction
export const reset = createAction('[Counter Component] decrement', props<{m: number}>());
2. reducerを定義
import { createReducer, on, Action } from '@ngrx/store';
import { decrement, increment, reset } from './counter.actions';
export const featureName = 'counter';
export interface State {
count: number;
}
export const initialState: State = {
count: 0,
};
const _counterReducer = createReducer(
initialState,
on(increment, (state) => ({ ...state, count: state.count + 1 })),
on(decrement, (state) => ({ ...state, count: state.count - 1 })),
on(reset, (state, { m }) => ({ ...state, count: m }))
);
export const counterReducer = (state: State, action: Action) =>
_counterReducer(state, action);
3. selectorを定義
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { featureName, State } from './counter.reducer';
export const selectCounterState = createFeatureSelector<State>(featureName);
export const selectCount = createSelector(
selectCounterState,
(state) => state.count
);
4. effectsを定義
import { Injectable } from '@angular/core';
import { Actions } from '@ngrx/effects';
import { createEffect, ofType } from '@ngrx/effects';
import { tap } from 'rxjs/operators';
import { increment } from './counter.actions';
@Injectable()
export class CounterEffects {
constructor(private actions$: Actions) {}
increment$ = createEffect(
() =>
this.actions$.pipe(
ofType(increment), # 発火してほしいアクションを入れる
tap(() => console.log('icrementするたびに呼ばれるよ'))
),
{ dispatch: false }
);
}
5. モジュールに登録
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { StoreModule } from '@ngrx/store';
import { counterReducer, featureName } from './counter.reducer';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { EffectsModule } from '@ngrx/effects';
import { CounterEffects } from './counter.effect';
@NgModule({
imports: [
BrowserModule,
StoreModule.forRoot({ [featureName]: counterReducer }), # feature rootもある
EffectsModule.forRoot([CounterEffects]), # feature rootもある
StoreDevtoolsModule.instrument(), # devtool用
],
declarations: [AppComponent],
bootstrap: [AppComponent],
})
export class AppModule {}
6. コンポーネントから呼ぶ
import { Component } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { State } from './counter.reducer';
import { selectCount } from './counter.selector';
import * as CounterActions from './counter.actions';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
constructor(private store: Store<State>) {}
count$ = this.store.pipe(select(selectCount));
increment() {
this.store.dispatch(CounterActions.increment());
}
decrement() {
this.store.dispatch(CounterActions.decrement());
}
reset() {
this.store.dispatch(CounterActions.reset(0));
}
}
7. HTMLから呼ぶ
<div>
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
<button (click)="reset()">reset</button>
<p>{{ count$ | async }}</p>
</div>