angularngrx-store-4.0

ngrx store 4 select not working


I was struggling the whole day to get my Angular app working with ngrx/store 4. After lots of unsuccessful hours, I decided to create a new absolute minimal app to check if I have maybe another problem. But the minimal app is not working at all :(

In the Chrome Redux devtools I can see, that the store is updated properly. But the UI is not updated. Also the log statement in the app.component.ts (this.age$.subscribe(console.log)) is never executed. I really have no clue what I am missing.

Here is the relevant code.

package.json

{
  "dependencies": {
    "@angular/animations": "^4.2.4",
    "@angular/common": "^4.2.4",
    "@angular/compiler": "^4.2.4",
    "@angular/core": "^4.2.4",
    "@angular/forms": "^4.2.4",
    "@angular/http": "^4.2.4",
    "@angular/platform-browser": "^4.2.4",
    "@angular/platform-browser-dynamic": "^4.2.4",
    "@angular/router": "^4.2.4",
    "@ngrx/store": "^4.0.2",
    "@ngrx/store-devtools": "^4.0.0",
    "rxjs": "^5.4.2",
    "zone.js": "^0.8.14"
  },
  "devDependencies": {
    "@types/node": "^8.0.22",
    "typescript": "^2.4.2"
  }
}

app.module.ts

import { AppState } from './state';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools'
import { reducer } from './reducer';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    StoreModule.forRoot({ reducer }),
    StoreDevtoolsModule.instrument(),
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

state.ts

export interface AppState {
  age: number
}

export const INITIAL_STATE: AppState = {
  age: 1
}

actions.ts

import { Action } from '@ngrx/store';

export const ACTION_TYPE = 'CHANGE_AGE_ACTION';

export class ChangeAgeAction implements Action {
  readonly type: string = ACTION_TYPE;

  constructor(public payload?: number) {
  }
}

reducer.ts

import { ChangeAgeAction } from './actions';
import { Action } from '@ngrx/store';
import { AppState, INITIAL_STATE } from './state';
import { ACTION_TYPE } from './actions';

function changeName(state: AppState, action: ChangeAgeAction): AppState {
  return {
    age: action.payload
  }
}

export function reducer(state: AppState = INITIAL_STATE, action: Action) {
  switch (action.type) {
    case ACTION_TYPE:
      return changeName(state, action);

    default:
      return state;
  }
}

app.component.ts

import { ChangeAgeAction } from './actions';
import { AppState } from './state';
import { Component } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Store } from '@ngrx/store';
import { map } from 'rxjs/operator/map';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  age$: Observable<number>;

  constructor(private store: Store<AppState>) {
    this.age$ = this.store.select<number>(state => state.age);
    this.age$.subscribe(console.log);
  }

  changeAge() {
    this.store.dispatch(new ChangeAgeAction(Math.random()));
  }
}

app.component.html

<h3>Age: {{ age$ | async }}</h3>
<button (click)="changeAge()">Change Age</button>

Solution

  • Your app state isn't modeled properly. There's actually a reducer object that contains your age. Here's the changes I made to make the app work:

    In state.ts

    export interface AppState {
      reducer: {
        age: number
      }
    }
    

    In app component:

    export class AppComponent {
      age$: Observable<number>;
    
      constructor(private store: Store<AppState>) {
        this.age$ = this.store.select<number>(state => state.reducer.age);
        this.age$.subscribe(console.log);
      }
    
      changeAge() {
        this.store.dispatch(new ChangeAgeAction(Math.random()));
      }
    }
    

    Note the reason that you have the "reducer" object in your AppState is because when you setup doing this: StoreModule.forRoot({ reducer }). It gets the "reducer" name from reducer.

    In other words if you did something like StoreModule.forRoot({ ageReducer }) then your app state would look like this:

    export interface AppState {
      ageReducer: {
        age: number
      }
    }
    

    Here's a working version of the code:

    https://stackblitz.com/edit/angular-jxszwh