Intro
This time, I tried Effects(@ngrx/effects).[Angular] Try NgRx 1
Effects wasn't for side-effects.
It was for interaction with external resources such as network requests.
And it could fire actions like "store.dispatch" to Reducers.
I thought it worked like this.
Install
npx ng add @ngrx/effects
Environments
- Angular: 10.0.2
- @ngrx/store: 9.2.0
- @ngrx/effects: 9.2.0
Add services
In this sample, I added a service to check the game had been finished. And it didn't send and receive network request.check-game-finished.ts
import { SquareValue } from './board/square/square-value';
import { GameResult } from './game-result';
type CheckTarget = {
index: number,
match: readonly CheckTarget[],
unmatch: readonly CheckTarget[]
};
const emptyTarget = {
match: [],
unmatch: []
};
const checkTargets: CheckTarget = {
index: 0,
match: [{
index: 1,
match: [{
index:2,
...emptyTarget
}],
unmatch: []
}, {
index: 3,
match: [{
index: 6,
...emptyTarget
}],
unmatch: []
}, {
index: 4,
match: [{
index: 8,
...emptyTarget
}],
unmatch: []
}],
unmatch: [{
index: 4,
match: [{
index: 1,
match: [{
index: 7,
...emptyTarget
}, {
index: 3,
match: [{
index: 5,
...emptyTarget
}],
unmatch: []
}],
unmatch: []
}],
unmatch:[{
index: 8,
match: [{
index: 2,
...emptyTarget
},{
index: 6,
...emptyTarget
}],
unmatch: []
}]
}]
};
function getTargetIndices(squares: readonly SquareValue[]): { circles: number[], crosses: number[]} {
const circles = new Array<number>();
const crosses = new Array<number>();
for(let i = 0; i < squares.length; i++) {
if (squares[i] == '◯') {
circles.push(i);
} else if(squares[i] == '✕') {
crosses.push(i);
}
}
return { circles, crosses };
}
function check(targetIndices: readonly number[],
nextTarget: CheckTarget): boolean {
if (targetIndices.some(i => nextTarget.index == i)) {
if (nextTarget.match.length <= 0) {
return true;
}
if (nextTarget.match.some(t => check(targetIndices, t))) {
return true;
}
} else {
if (nextTarget.unmatch.length <= 0) {
return false;
}
if (nextTarget.unmatch.some(t => check(targetIndices, t))) {
return true;
}
}
return false;
}
export function checkGameFinished(squares: readonly SquareValue[]): GameResult {
console.log("check");
const targets = getTargetIndices(squares);
if (check(targets.circles, checkTargets)) {
return { finished: true, winner: '◯' };
} else if (check(targets.crosses, checkTargets)) {
return { finished: true, winner: '✕' };
}
return { finished: false, winner: null };
}
These functions were for checking the game had already finished.
I wanted to rewrite more simple way...
game-score.service.ts
import { Injectable } from '@angular/core';
import { SquareValue } from './board/square/square-value';
import { Observable, of } from 'rxjs';
import { GameResult } from './game-result';
import * as GameChecker from './check-game-finished';
@Injectable({
providedIn: 'root'
})
export class GameScoreService {
constructor() { }
public checkResult(squares: readonly SquareValue[]): Observable<GameResult> {
return of(GameChecker.checkGameFinished(squares));
}
}
Add Effects
game-result.effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { map, mergeMap, catchError } from 'rxjs/operators';
import { GameScoreService } from './game-score.service';
import { checkScore, gameFinished } from './game.actions';
import { EMPTY } from 'rxjs';
@Injectable()
export class GameResultEffects {
/* this would be fired by dispatching "checkScore" action. */
checkResult$ = createEffect(() => this.actions$.pipe(
ofType(checkScore),
/* call service */
mergeMap(action => this.gameScoreService.checkResult(action.squares)
.pipe(
/* make an action fired */
map(result => gameFinished({ result })),
catchError(error => {
console.error(error);
return EMPTY;
})
))));
constructor(
private actions$: Actions,
private gameScoreService: GameScoreService
) {}
}
game.action.ts
import { createAction, props } from '@ngrx/store';
import { SquareValue } from './board/square/square-value';
import { GameResult } from './game-result';
...
export const checkScore = createAction('[Score] checkScore',
props<{ squares: readonly SquareValue[]}>());
export const gameFinished = createAction('[Game] finished',
props<{ result: GameResult }>());
Register effects
app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { StoreModule } from '@ngrx/store';
import { BoardComponent } from './tic-tac-toe/board/board.component';
import { SquareComponent } from './tic-tac-toe/board/square/square.component';
import * as gameReducer from './tic-tac-toe/game.reducer';
import { EffectsModule } from '@ngrx/effects';
import { GameResultEffects } from './tic-tac-toe/game-result.effects';
@NgModule({
declarations: [
AppComponent,
BoardComponent,
SquareComponent
],
imports: [
BrowserModule,
StoreModule.forRoot({game: gameReducer.reducer}),
EffectsModule.forRoot([GameResultEffects])
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Connect with component(failed)
I add selector into the component to get Effects result.board.component.ts
import { Component, OnInit } from '@angular/core';
import { BoardState } from './board-state';
import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs';
import { updateSquare, checkScore } from '../game.actions';
import { GameResult } from '../game-result';
import { SquareValue } from './square/square-value';
@Component({
selector: 'app-board',
templateUrl: './board.component.html',
styleUrls: ['./board.component.css']
})
export class BoardComponent implements OnInit {
public state$: Observable<BoardState>;
public result$: Observable<GameResult>;
constructor(private store: Store<{game: BoardState, result: GameResult }>) {
this.state$ = store.pipe(select('game'));
this.state$.subscribe(s => this.onValueChanged(s.squares));
this.result$ = store.select(state => state.result);
this.result$.subscribe(r => console.log(r));
}
ngOnInit(): void {
}
public updateSquare(index: number) {
this.store.dispatch(updateSquare({index}));
}
private onValueChanged(squares: readonly SquareValue[]) {
this.store.dispatch(checkScore({squares}));
}
}
The way of firing Effects actions were same as Reducer.
Couldn't get result
Though I could get result from "this.gameScoreService", "this.result$.subscribe(r => console.log(r));" wasn't called. The reason was I hadn't added a Reducer to handling the "gameFinished" action. I thought Effects didn't connect with Store directly. So I added a Reducer.game-result.reducer.ts
import { GameResult } from './game-result';
import { createReducer, on, Action } from '@ngrx/store';
import { gameFinished } from './game.actions';
function initialState(): GameResult {
return {
finished: false,
winner: null
};
}
const _resultReducer = createReducer(initialState(),
on(gameFinished, (state, {result}) => {
if (result.finished) {
return result;
}
return state;
}));
export function reducer(state: GameResult | undefined, action: Action) {
return _resultReducer(state, action);
}
app.module.ts
...
import * as gameResultReducer from './tic-tac-toe/game-result.reducer';
import { EffectsModule } from '@ngrx/effects';
import { GameResultEffects } from './tic-tac-toe/game-result.effects';
@NgModule({
...
imports: [
BrowserModule,
StoreModule.forRoot({game: gameReducer.reducer, result: gameResultReducer.reducer}),
EffectsModule.forRoot([GameResultEffects])
],
...
Now I could get result of "this.gameScoreService" in the component :).
コメント
コメントを投稿