@adrian.afergon
Adrián Ferrera González
@adrian.afergon
@afergon
adrian-afergon
@adrian.afergon
@adrian.afergon
@adrian.afergon
State
Actions
Reducers
Selectors
Streams
Example !== Reality
@adrian.afergon
@adrian.afergon
@adrian.afergon
Brandon Roberts
Mike Ryan
@adrian.afergon
@adrian.afergon
@adrian.afergon
Technology | redux-pattern | Side-effects |
---|---|---|
React | Redux | Redux-observables |
Angular | NgRx | Effects |
Vue | Vuex | Vue-RX |
@adrian.afergon
ng add @ngrx/store
ng add @ngrx/effects
ng add @ngrx/store-devtools
@adrian.afergon
@NgModule({
imports: [
CommonModule,
RouterModule,
SharedModule,
HttpClientModule,
CoreRouter,
StoreModule.forRoot(reducers),
EffectsModule.forRoot([AppEffects]),
StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: environment.production })
],
declarations: [HeaderComponent, MainComponent, FooterComponent],
exports: [MainComponent]
})
export class CoreModule { }
@adrian.afergon
export interface GauntletState {
powerStone: Stone;
spaceStone: Stone;
realityStone: Stone;
mindStone: Stone;
soulStone: Stone;
timeStone: Stone;
}
For your Store / GlobalState
@adrian.afergon
export const EQUIP = '[Gauntlet] equip';
export class Equip implements Action {
readonly type = EQUIP;
constructor(public stoneId: string) {}
}
export type GauntletActions
= Equip;
@adrian.afergon
export function gauntletReducer(state: GauntletState = defaultState, action: GauntletActions) {
switch (action.type) {
case GET_STONES_FULFILLED:
return {...state, ...action.gauntlet};
case EQUIP:
const stone = state[action.stoneId];
return {...state, [action.stoneId]: {...stone, equipped: !stone.equipped} };
default:
return state;
}
}
@adrian.afergon
@Component({
selector: 'app-gauntlet',
templateUrl: './gauntlet.component.html',
styleUrls: ['./gauntlet.component.scss']
})
export class GauntletComponent implements OnInit {
public stones$: Observable<Array<StoneModel>>;
public isGauntletCompleted$: Observable<boolean>;
constructor(private store: Store<GauntletState>) {
this.stones$ = store.pipe(select(GauntletSelectors.getGauntletAsViewModel));
this.isGauntletCompleted$ = store.pipe(select(GauntletSelectors.isGauntletCompleted));
}
ngOnInit() {
this.store.dispatch(new GetStones());
}
public onSnap() {
this.store.dispatch(new Snap());
}
public equipStone(stoneId) {
this.store.dispatch(new Equip(stoneId));
}
}
@adrian.afergon
export class GauntletSelectors {
static getGauntletState = createFeatureSelector<GauntletState>('gauntlet');
static getGauntletAsViewModel = createSelector(GauntletSelectors.getGauntletState,
(state): StoneModel[] => GauntletMapper.toViewModel(state));
static isGauntletCompleted = createSelector(GauntletSelectors.getGauntletAsViewModel,
stones => !!stones.find(stone => !stone.equipped));
}
@adrian.afergon
@Injectable()
export class AppEffects {
constructor(
private actions$: Actions,
private stoneRepository: StonesRepository,
private socketClient: SocketClient) {}
@Effect()
getStones: Observable<Action> = this.actions$.pipe(
ofType(GET_STONES),
switchMap(action => this.stoneRepository.getStones()),
map((gauntlet: Gauntlet) => new GetStonesFulfilled(gauntlet)),
catchError( error => of(new GetStonesRejected(error)))
);
@Effect({dispatch: false})
onSnap = this.actions$.pipe(
ofType(SNAP),
tap( () => this.socketClient.snap())
);
}
@adrian.afergon
@adrian.afergon
@adrian.afergon
$ git clone https://github.com/adrian-afergon/jsDayCan18-infinite-wars.git
$ cd jsDayCan18-infinite-wars
$ npm install
$ ng serve
$ git checkout step-1
Clone and install
Chrome redux dev-tools
Configure module
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { StoreModule } from '@ngrx/store';
import { reducers } from './reducers';
import { EffectsModule } from '@ngrx/effects';
import { AppEffects } from './app.effects';
import { CoreModule } from './core/core.module';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { environment } from '../environments/environment';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
CoreModule,
StoreModule.forRoot(reducers),
EffectsModule.forRoot([AppEffects]),
StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: environment.production })
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
@adrian.afergon
$ git checkout step-2
Define Gauntlet State
import { ActionReducerMap } from '@ngrx/store';
import {Stone} from '../core/model/stones';
interface GauntletState {
powerStone: Stone;
spaceStone: Stone;
realityStone: Stone;
mindStone: Stone;
soulStone: Stone;
timeStone: Stone;
}
export interface State {
gauntlet: GauntletState;
}
export const reducers: ActionReducerMap<State> = {
};
@adrian.afergon
$ git checkout step-3
Return initial state hacked
//reducers/gauntlet.reducer.ts
import {Action} from '@ngrx/store';
import {Stone} from '../core/model/stones';
import {gauntlet} from './gauntlet.hack';
export interface GauntletState {
powerStone: Stone;
spaceStone: Stone;
realityStone: Stone;
mindStone: Stone;
soulStone: Stone;
timeStone: Stone;
}
export function gauntletReducer(state: GauntletState = gauntlet, action: Action) {
switch (action.type) {
default:
return state;
}
}
@adrian.afergon
Map reducer
//reducers/index.ts
import {ActionReducerMap} from '@ngrx/store';
import {gauntletReducer, GauntletState} from './gauntlet.reducer';
export interface State {
gauntlet: GauntletState;
}
export const reducers: ActionReducerMap<State> = {
gauntlet: gauntletReducer
};
@adrian.afergon
$ git checkout step-4
Display values
// container/gauntlet.component.ts
import {Component, OnDestroy, OnInit} from '@angular/core';
import { SocketClient } from '../../../core/services/socket.client';
import { StonesRepository } from '../../../core/services/stones.repository';
import { StoneModel } from '../../viewmodel/Stone.model';
import { GauntletMapper } from '../../mappers/gauntlet.mapper';
import {Observable} from 'rxjs';
import {select, Store} from '@ngrx/store';
import {GauntletState} from '../../../reducers/gauntlet.reducer';
@Component({
selector: 'app-gauntlet',
templateUrl: './gauntlet.component.html',
styleUrls: ['./gauntlet.component.scss']
})
export class GauntletComponent implements OnInit, OnDestroy {
public stones: StoneModel[];
public stones$: Observable<GauntletState>;
// public subscription: Subscription;
constructor(
private store: Store<GauntletState>,
private socketClient: SocketClient,
private stonesRepository: StonesRepository) {
this.stones = [];
this.stones$ = store.pipe(select('gauntlet'));
}
ngOnInit() {
// this.subscription = this.stonesRepository.getStones()
// .subscribe(gauntlet => this.stones = GauntletMapper.toViewModel(gauntlet));
}
ngOnDestroy() {
// this.subscription.unsubscribe();
}
public isGauntletCompleted() {
return !!this.stones.find(stone => !stone.equipped);
}
public onSnap() {
this.socketClient.snap();
}
public equipStone(stoneId) {
this.stones.map( stone => stone.id === stoneId ? stone.equipped = !stone.equipped : stone);
}
public toViewModel(data) {
console.log('re-render...');
return GauntletMapper.toViewModel(data);
}
}
@adrian.afergon
Display values
<!-- container/gauntlet.component.html -->
<div id="gauntlet">
<div class="gauntlet-content">
<app-infinity-gauntlet [disabled]="isGauntletCompleted()"
(onSnap)="onSnap()"></app-infinity-gauntlet>
</div>
<div class="stone-list" *ngIf="stones$ | async as stones">
<app-stone *ngFor="let stone of toViewModel(stones)"
[stone]="stone"
(onClick)="equipStone($event)" ></app-stone>
</div>
</div>
@adrian.afergon
Problems...
@adrian.afergon
$ git checkout step-5
Use selectors for map the state to view-model
// reducers/gauntlet.reducer.ts
const getGauntletState = createFeatureSelector<GauntletState>('gauntlet');
export const getGauntletAsViewModel =
createSelector(getGauntletState, (state): StoneModel[] => GauntletMapper.toViewModel(state));
Change Component
// container/gauntlet.component.ts
// [...]
export class GauntletComponent implements OnInit {
public stones: StoneModel[];
public stones$: Observable<any>;
constructor(private store: Store<GauntletState>, private socketClient: SocketClient, private stonesRepository: StonesRepository) {
this.stones = [];
this.stones$ = store.pipe(select(getGauntletAsViewModel));
store.pipe(select(getGauntletAsViewModel)).subscribe(value => {
console.log('re-render');
});
}
// [...]
}
@adrian.afergon
$ git checkout step-6
Refactor selectors
// selectors/gauntlet.selectors.ts
import {createFeatureSelector, createSelector} from '@ngrx/store';
import {StoneModel} from '../gauntlet/viewmodel/Stone.model';
import {GauntletMapper} from '../gauntlet/mappers/gauntlet.mapper';
import {GauntletState} from '../reducers/gauntlet.reducer';
export class GauntletSelectors {
static getGauntletState = createFeatureSelector<GauntletState>('gauntlet');
static getGauntletAsViewModel = createSelector(GauntletSelectors.getGauntletState,
(state): StoneModel[] => GauntletMapper.toViewModel(state));
static isGauntletCompleted = createSelector(GauntletSelectors.getGauntletAsViewModel,
stones => !!stones.find(stone => !stone.equipped));
}
@adrian.afergon
Change the container
import {Component, OnInit} from '@angular/core';
import { SocketClient } from '../../../core/services/socket.client';
import { StonesRepository } from '../../../core/services/stones.repository';
import { StoneModel } from '../../viewmodel/Stone.model';
import {Observable} from 'rxjs';
import {select, Store} from '@ngrx/store';
import {GauntletState} from '../../../reducers/gauntlet.reducer';
import {GauntletSelectors} from '../../../selectors/gauntlet.selectors';
@Component({
selector: 'app-gauntlet',
templateUrl: './gauntlet.component.html',
styleUrls: ['./gauntlet.component.scss']
})
export class GauntletComponent implements OnInit {
public stones: StoneModel[];
public stones$: Observable<Array<StoneModel>>;
public isGauntletCompleted$: Observable<boolean>;
constructor(
private store: Store<GauntletState>,
private socketClient: SocketClient,
private stonesRepository: StonesRepository) {
this.stones = [];
this.stones$ = store.pipe(select(GauntletSelectors.getGauntletAsViewModel));
this.isGauntletCompleted$ = store.pipe(select(GauntletSelectors.isGauntletCompleted));
}
ngOnInit() {
// this.subscription = this.stonesRepository.getStones()
// .subscribe(gauntlet => this.stones = GauntletMapper.toViewModel(gauntlet));
}
public onSnap() {
this.socketClient.snap();
}
public equipStone(stoneId) {
this.stones.map( stone => stone.id === stoneId ? stone.equipped = !stone.equipped : stone);
}
}
@adrian.afergon
And the view
<div id="gauntlet">
<div class="gauntlet-content">
<app-infinity-gauntlet
[disabled]="isGauntletCompleted$ | async"
(onSnap)="onSnap()"></app-infinity-gauntlet>
</div>
<div class="stone-list" *ngIf="stones$ | async as stones">
<app-stone *ngFor="let stone of stones"
[stone]="stone"
(onClick)="equipStone($event)" ></app-stone>
</div>
</div>
@adrian.afergon
$ git checkout step-7
Define actions for mutate out state
export const EQUIP = '[Gauntlet] equip';
export const SNAP = '[Gauntlet] snap';
(...)
@adrian.afergon
(...)
@adrian.afergon
Typescript the rescue
import {Action} from '@ngrx/store';
export const EQUIP = '[Gauntlet] equip';
export const SNAP = '[Gauntlet] snap';
export class Equip implements Action {
readonly type = EQUIP;
constructor(public stoneId: string) {}
}
export class Snap implements Action {
readonly type = SNAP;
constructor() {}
}
export type GauntletActions
= Equip
| Snap;
@adrian.afergon
import {Stone} from '../core/model/stones';
import {gauntlet} from './gauntlet.hack';
import {EQUIP, GauntletActions} from '../actions/gauntlet.actions';
export interface GauntletState {
powerStone: Stone;
spaceStone: Stone;
realityStone: Stone;
mindStone: Stone;
soulStone: Stone;
timeStone: Stone;
}
export function gauntletReducer(state: GauntletState = gauntlet, action: GauntletActions) {
switch (action.type) {
case EQUIP:
const stone = state[action.stoneId];
return {...state, [action.stoneId]: {...stone, equipped: !stone.equipped} };
default:
return state;
}
}
@adrian.afergon
@adrian.afergon
Now the container
// [...]
import {Equip} from '../../../actions/gauntlet.actions';
@Component({
selector: 'app-gauntlet',
templateUrl: './gauntlet.component.html',
styleUrls: ['./gauntlet.component.scss']
})
export class GauntletComponent implements OnInit {
public stones$: Observable<Array<StoneModel>>;
public isGauntletCompleted$: Observable<boolean>;
// [...]
public equipStone(stoneId) {
this.store.dispatch(new Equip(stoneId));
}
}
@adrian.afergon
$ git checkout step-8
Effects
PART - 1
Create the actions
import {Action} from '@ngrx/store';
import {Gauntlet} from '../core/model/stones';
export const EQUIP = '[Gauntlet] equip';
export const GET_STONES = '[Gauntlet] get stones';
export const GET_STONES_FULFILLED = '[Gauntlet] get stones fulfilled';
export const GET_STONES_REJECTED = '[Gauntlet] get stones rejected';
export class GetStones implements Action {
readonly type = GET_STONES;
constructor() {}
}
export class GetStonesFulfilled implements Action {
readonly type = GET_STONES_FULFILLED;
constructor(public gauntlet: Gauntlet) {}
}
export class GetStonesRejected implements Action {
readonly type = GET_STONES_REJECTED;
constructor(public error: string) {}
}
export class Snap implements Action {
readonly type = SNAP;
constructor() {}
}
export type GauntletActions
= Equip
| GetStones
| GetStonesFulfilled
| GetStonesRejected
| Snap;
@adrian.afergon
$ git checkout step-9
Effects
PART - 2
Create the effect
@Injectable()
export class AppEffects {
constructor(
private actions$: Actions,
private stoneRepository: StonesRepository,
private socketClient: SocketClient) {}
@Effect()
getStones: Observable<Action> = this.actions$.pipe(
ofType(GET_STONES),
switchMap(action => this.stoneRepository.getStones()),
map((gauntlet: Gauntlet) => new GetStonesFulfilled(gauntlet)),
catchError( error => of(new GetStonesRejected(error)))
);
}
@adrian.afergon
$ git checkout step-10
Effects
PART - 3
Create the effect for snap
@Injectable()
export class AppEffects {
constructor(
private actions$: Actions,
private stoneRepository: StonesRepository,
private socketClient: SocketClient) {}
// [...]
@Effect({dispatch: false})
onSnap = this.actions$.pipe(
ofType(SNAP),
tap( () => this.socketClient.snap())
);
}
@adrian.afergon
$ git checkout step-11
Create feature reducer
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PopulationComponent } from './containers/population/population.component';
import {PopulationRouter} from './population.routing';
import {SharedModule} from '../shared/shared.module';
import {StoreModule} from '@ngrx/store';
import {EffectsModule} from '@ngrx/effects';
import {populationReducer} from './infrastructure/reducers/';
@NgModule({
imports: [
CommonModule,
SharedModule,
PopulationRouter,
StoreModule.forFeature('population', populationReducer),
EffectsModule.forFeature([])
],
declarations: [PopulationComponent]
})
export class PopulationModule { }
@adrian.afergon
Create feature reducer
import {Action} from '@ngrx/store';
interface PopulationState {
people: number;
}
const defaultState: PopulationState = {
people: 0
};
export function populationReducer(state: PopulationState = defaultState, action: Action) {
switch (action.type) {
default:
return state;
}
}
@adrian.afergon
Complete the population container using the knwoledge lerned
$ git checkout step-12
$ git checkout result
@adrian.afergon
Angular Firebase: https://www.youtube.com/channel/UCsBjURrPoezykLs9EqgamOA
Testing NgRx:
LeanMind Medium profile:
And my Medium profile:
@adrian.afergon
@adrian.afergon
@adrian.afergon