スキップしてメイン コンテンツに移動

[Angular] Try NgRx 2

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 :).

コメント

このブログの人気の投稿

[Angular][ASP.NET Core] Upload chunked files

Intro I wanted to send files to Web application (made by ASP.NET Core). If the file size had been small, I didn't need do any special things. But when I tried to send a large file, the error was occurred by ASP.NET Core's limitation. Though I could change the settings, but I didn't want to do that, because I hadn't known the file sizes what would been actually using. So I splitted the data into chunks first, and sent them. After receiving all chunks, I merged them into one file. There might be some libraries or APIs (ex. Stream API) what did them automatically, but I couldn't find them. What I did [ASP.NET Core] Make CORS enabled [Angular] Split a large file into chunks [Angular][ASP.NET Core] Send and receive data as form data [ASP.NET Core] Merge chunks into one file [ASP.NET Core] Make CORS enabled Because the client side application(Angular) and the server side application(ASP.NET Core) had been separated, I had to make CORS(Cross-Origin Requests)

[Nest.js] Use WebSocket with ws

Intro Until last time , I had used node-web-rtc to try WebRTC. But because the example was a little complicated for I understood the core functions of using WebRTC. So I look for other frameworks or libraries. PeerJS is a famous library for WebRTC. peers/peerjs: Peer-to-peer data in the browser. - GitHub peers/peerjs-server: Server for PeerJS - GitHub PeerJS - Simple peer-to-peer with WebRTC A problem is I don't know how to integrate to the Nest.js project. I couldn't find examples. So I don't choose at least this time. What shall I choose? According MDN, WebRTC doesn't specify strictly what technology is used on server application for connecting two devices. Signaling and video calling - Web APIs | MDN But in many examples include MDN's one use WebSocket. samples-server/s/webrtc-from-chat at master · mdn/samples-server · GitHub So I try WebSocket in the Nest.js project. Use WebSocket in a Nest.js project Nest.js has a function for using We

[Nest.js] Show static files

Intro I wanted to use Nest.js and WebRTC(node-webrtc). NestJS - A progressive Node.js framework Documentation | NestJS - A progressive Node.js framework And because I wanted to try with simple page(not use JavaScript frameworks), I added static HTML, CSS, JavaScript into a Nest.js project. Prepare Install First, I installed @nestjs/cli. First steps | NestJS - A progressive Node.js framework As same as last time , I couldn't do global install because I had used Volta. But I could installed by volta. volta install @nestjs/cli Create project nest new nest-web-rtc-sample volta pin node@12 Run npm start After doing "npm start", I could getting "Hello World!" from http://localhost:3000. Add static files I could add static files by two ways. @nestjs/serve-static First one of them was using "serve-static". Serve Static | NestJS - A progressive Node.js framework npm install --save @nestjs/serve-static And I needed adding a module into app.modu