ngrx does not maintain state

660 Views Asked by At

Using an own small store implementation (which worked) I am trying to introduce ngrx/store into an angular 6 app. Versions are:

"@ngrx/core": "^1.2.0",
"@ngrx/store": "^6.1.0"

The application is an angular frontend which gets/writes data to a database via a Spring application through http. When the BankaccountListComponent is first loaded it extracts the list of bankaccounts from the database via the BankaccountService and sucessively the spring application. The BankaccountService writes the extracted data into the store. Via an observable the BankaccountListComponent is connected to the store.

However, it does not get the data. In the client there is only an empty box displayed. It looks like the observable bankaccounts$ is never updated by the store.

Possibly the error is obvious for you by just looking through the code below. If you would like me to create a runnable app e.g. in jsfiddle please let me know and I will do.

console output is

bankaccountReducer: default action Array [] bankaccount.reducer.ts:24:6
Angular is running in the development mode. Call enableProdMode() to enable the production mode. core.js:3121
BankaccountListComponent: ngOnInit bankaccount-list.component.ts:20:4
BankaccountListComponent: leaving ngOnInit bankaccount-list.component.ts:23:4
BankaccountService: fetched 2 bankaccounts from server bankaccount.service.ts:30:6
BankaccountService: bankaccounts retrieved: Array [ {…}, {…} ] bankaccount.service.ts:33:6
bankaccountReducer: loading data: Array [ {…}, {…} ] bankaccount.reducer.ts:7:6
BankaccountService: bankaccounts dispatched
BankaccountItemComponent: got bankaccount undefined

bankaccount-list.component.html

<div class="bankaccount-list">
  <h1>Bank Accounts</h1>
  <div>
    <app-bankaccount-item *ngFor="let bankaccount of bankaccounts$ | async"
                          [bankaccount]="bankaccount">
    </app-bankaccount-item>
  </div>
</div>

bankaccount-list.component.ts

import {Component, OnDestroy, OnInit} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {BankaccountService} from '../../services/bankaccount.service';
import {Bankaccount} from '../../model/bankaccount';

@Component({
  selector: 'app-bankaccount-list',
  templateUrl: './bankaccount-list.component.html',
  styleUrls: ['./bankaccount-list.component.scss']
})
export class BankaccountListComponent implements OnInit, OnDestroy {

  bankaccounts$: Observable<Bankaccount[]>;
  bankaccountSelectedId: number;

  constructor(private bankaccountService: BankaccountService) {
  }

  ngOnInit() {
    console.log('BankaccountListComponent: ngOnInit');
    this.bankaccounts$ = this.bankaccountService.bankaccounts$;
    this.bankaccountService.getBankaccounts();
    console.log('BankaccountListComponent: leaving ngOnInit');
  }

  ngOnDestroy() {
    console.log('BankaccountListComponent: destroy');
  }
}

bankaccount-item.component.ts

import {Component, EventEmitter, Input, Output} from '@angular/core';
import {Router} from '@angular/router';
import {Bankaccount} from '../../model/bankaccount';

@Component({
  selector: 'app-bankaccount-item',
  templateUrl: './bankaccount-item.component.html',
  styleUrls: ['./bankaccount-item.component.scss']
})
export class BankaccountItemComponent {

  @Input() selected: boolean;
  @Input() bankaccount: Bankaccount;

  @Output() bankAccountSelected = new EventEmitter();
  @Output() bankAccountDeleted = new EventEmitter();

  constructor() {
    console.log('BankaccountItemComponent: got bankaccount', this.bankaccount);
  }
}

bankaccount-item.component.html

<div class="fade-in bankaccount-item" [ngClass]="{selected : selected}" (click)="select()">
  <mat-card>
    <mat-card-content>
      <div class="row">

        <div class="col1">
      <span>
        <a [routerLink]="['./edit', bankaccount.id]"
           (click)="$event.stopPropagation();">{{bankaccount.name}}</a>
      </span>
          <br>
          <span class="grey">{{bankaccount.iban | iban}}</span>
        </div>

        <div class="col2">
          <button mat-button (click)="delete(); $event.stopPropagation()">
            <mat-icon>delete_forever</mat-icon>
          </button>
        </div>

      </div>
    </mat-card-content>
  </mat-card>
</div>

bankaccount.service.ts

import {Injectable} from '@angular/core';
import {HttpClient, HttpHeaders} from '@angular/common/http';

import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map';
import {Bankaccount} from '../model/bankaccount';

import {AppState} from '../model/app.state';
import {Store} from '@ngrx/store';
import * as BankaccountActions from './../store/actions/bankaccount.actions';

const BANKACCOUNTS_URL = 'http://localhost:8090/account/accounts/';

@Injectable()
export class BankaccountService {

  private headers = new HttpHeaders();

  bankaccounts$: Observable<Bankaccount[]>;

  constructor(private httpClient: HttpClient, private store: Store<AppState>) {
    this.headers = this.headers.set('Content-Type', 'application/json');
    this.headers = this.headers.set('Accept', 'application/json');
    this.bankaccounts$ = store.select('bankaccounts');
  }

  getBankaccounts() {
    this.httpClient.get<Bankaccount[]>(BANKACCOUNTS_URL).map((result: any) => {
      console.log('BankaccountService: fetched ' + result._embedded.accounts.length + ' bankaccounts from server');
      return result._embedded.accounts.sort((a, b) => a.id - b.id);
    }).subscribe((bankaccounts) => {
      console.log('BankaccountService: bankaccounts retrieved: ', bankaccounts);
      this.store.dispatch(new BankaccountActions.LoadBankaccounts(bankaccounts));
      console.log('BankaccountService: bankaccounts dispatched');
    });
  }
}

bankaccount.actions.ts

import {Action} from '@ngrx/store';
import {Bankaccount} from '../../model/bankaccount';

export enum BankaccountActionTypes {
  LOAD_BANKACCOUNT = '[Bankaccount] Load',
  ADD_BANKACCOUNT = '[Bankaccount] Add',
  UPDATE_BANKACCOUNT = '[Bankaccount] Update',
  REMOVE_BANKACCOUNT = '[Bankaccount] Remove'
}

export class LoadBankaccounts implements Action {
  readonly type = BankaccountActionTypes.LOAD_BANKACCOUNT;

  constructor(public payload: Bankaccount) {
  }
}

export class AddBankaccount implements Action {
  readonly type = BankaccountActionTypes.ADD_BANKACCOUNT;

  constructor(public payload: Bankaccount) {
  }
}

export class UpdateBankaccount implements Action {
  readonly type = BankaccountActionTypes.UPDATE_BANKACCOUNT;

  constructor(public payload: Bankaccount) {
  }
}

export class RemoveBankaccount implements Action {
  readonly type = BankaccountActionTypes.REMOVE_BANKACCOUNT;

  constructor(public payload: Bankaccount) {
  }
}

export type Actions = LoadBankaccounts | AddBankaccount | UpdateBankaccount | RemoveBankaccount;

bankaccount.reducer.ts

import {Bankaccount} from '../../model/bankaccount';
import * as BankaccountActions from './../actions/bankaccount.actions';

export function bankaccountReducer(state: Bankaccount[] = [], action: BankaccountActions.Actions) {
  switch (action.type) {
    case BankaccountActions.BankaccountActionTypes.LOAD_BANKACCOUNT:
      console.log('bankaccountReducer: loading data: %o', action.payload);
      return [...state, action.payload];
    case BankaccountActions.BankaccountActionTypes.ADD_BANKACCOUNT:
      console.log('bankaccountReducer: adding data %o', action.payload);
      return [...state, action.payload];
    case BankaccountActions.BankaccountActionTypes.UPDATE_BANKACCOUNT:
      console.log('bankaccountReducer: updating data for %o', action.payload);
      return state.map(bankaccount => {
        if (bankaccount.id !== action.payload.id) {
          return bankaccount;
        }
        return action.payload;
      });
    case BankaccountActions.BankaccountActionTypes.REMOVE_BANKACCOUNT:
      console.log('bankaccountReducer: removing bankaccount %o', action.payload);
      return state.filter(bankaccount => bankaccount.id !== action.payload.id);
    default:
      console.log('bankaccountReducer: default action', state);
      return state;
  }
}

app.state.ts

import {Bankaccount} from './bankaccount';

export interface AppState {
  readonly bankaccounts: Bankaccount[];
}

app.module.ts

// ... other imports
import {StoreModule} from '@ngrx/store';
import {bankaccountReducer} from './store/reducer/bankaccount.reducer';
import {BankaccountService} from './services/bankaccount.service';

@NgModule({
  declarations: [
    // ... declarations which have nothing to do with store ...
  ],
  imports: [
    // other imports
    StoreModule.forRoot({bankaccounts: bankaccountReducer})
  ],
  providers: [
    BankaccountService,
    {provide: APP_BASE_HREF, useValue: '/account'}
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
}
0

There are 0 best solutions below