Search code examples
angularrxjsngrx

how can I access and print my state property from Store using async?


This is my Angular code for a simple counter, practicing on ngRx.

counter.reducer.ts:

import { createReducer, on } from '@ngrx/store';
import { plus, minus, reset } from './counter.actions';

export interface CounterState{
  counter: number;
};

export const initialState: CounterState = {
  counter: 0
};

export const CounterReducer = createReducer(
  initialState,
  on(plus, (state) => ( {...state, counter: state.counter + 1}) ),
  on(minus, (state) => ( {counter: state.counter - 1}) ),
  on(reset, (state) => ({counter: 0})),
);

counter.actions.ts:

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

export const plus = createAction('counter +');
export const minus = createAction('counter -');
export const reset = createAction('counter Reset');

counter.component.ts:

import { Component } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { CounterState } from '../counter.reducer';
import { minus, plus, reset } from '../counter.actions';
import { Observable, map } from 'rxjs';
import { AsyncPipe, CommonModule } from '@angular/common';


@Component({
  selector: 'counter',
  standalone: true,
  templateUrl: './counter.component.html',
  styleUrls: ['./counter.component.css'],
  imports: [CommonModule],
})
export class Counter {
  //c!:number;
  counterFinal$: Observable<number>; //= this.store.select("counter");
  
  constructor(private store: Store<CounterState>) {
    //this.store.select('counter').subscribe(data => this.c = data.counter);
    this.counterFinal$ = this.store.select("counter");
    
  }

  plus() {
    //this.count = this.count + 1;
    console.log('+');
    this.store.dispatch(plus());
  }

  minus() {
    //this.count = this.count - 1;
    console.log('-');
    this.store.dispatch(minus());
  }

  reset() {
    //this.count = 0;
    console.log('0');
    this.store.dispatch(reset());
  }
}

I provide the store on my main.ts like this:

bootstrapApplication(App, {
  providers: [
    provideStore({ counter: CounterReducer }),
    provideStoreDevtools({
      maxAge: 25, // Retains last 25 states
      logOnly: !isDevMode(), // Restrict extension to log-only mode
      autoPause: true, // Pauses recording actions and state changes when the extension window is not open
      trace: false, //  If set to true, will include stack trace for every dispatched action, so you can see it in trace tab jumping directly to that part of code
      traceLimit: 75, // maximum stack trace frames to be stored (in case trace option was provided as true)
      connectInZone: true, // If set to true, the connection is established within the Angular zone
    }),
  ],
});

And at counter.component.html

<div>Counτer : {{ counterFinal$ | async }}</div>
<button id="plus" (click)="plus()">+</button>
<button id="minus" (click)="minus()">-</button>
<button id="reset" (click)="reset()">Reset</button>

As a result I get this:

results

On ngRx Dev-tool my store looks to works fine. I just dont know how to get the counter property and display it on html (using async)

enter image description here

if I change the counter.component.html to this:

<div>Counτer : {{ (counterFinal$ | async).counter }}</div>
<button id="plus" (click)="plus()">+</button>
<button id="minus" (click)="minus()">-</button>
<button id="reset" (click)="reset()">Reset</button>

I get this error:

NG9: Property 'counter' does not exist on type 'number'. [plugin angular-compiler]

src/counter/counter.component.html:1:41:
  1 │ <div>Counτer : {{ (counterFinal$ |async).counter }}</div>
    ╵                                         ~~~~~~~

Error occurs in the template of component Counter.

src/counter/counter.component.ts:12:15:
  12 │   templateUrl: './counter.component.html',

Using Angular 17 with standalone component, and latest ngrx. Thanks in advance!


Solution

  • Setting the type for the returning state in the select method seems to fix the issue.

    We can also define an interface that contains details on the state property types.

      ...
      counterFinal$: Observable<CounterState>; //= this.store.select("counter");
    
      constructor(private store: Store<{ counter: CounterState }>) {
        //this.store.select('counter').subscribe(data => this.c = data.counter);
        this.counterFinal$ = this.store.select('counter');
      }
      ...
    

    FULL CODE:

    import { Component } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import 'zone.js';
    import {
      Action,
      ActionReducer,
      createReducer,
      on,
      provideStore,
    } from '@ngrx/store';
    import { Store, select } from '@ngrx/store';
    import { Observable, map } from 'rxjs';
    import { AsyncPipe, CommonModule } from '@angular/common';
    import { createAction } from '@ngrx/store';
    
    export interface CounterState {
      counter: number;
    }
    
    export const initialState: CounterState = {
      counter: 0,
    };
    
    export const plus = createAction('counter +');
    export const minus = createAction('counter -');
    export const reset = createAction('counter Reset');
    
    export const CounterReducer = createReducer<
      CounterState,
      Action,
      ActionReducer<CounterState, Action>
    >(
      initialState,
      on(plus, (state) => ({ ...state, counter: state.counter + 1 })),
      on(minus, (state) => ({ counter: state.counter - 1 })),
      on(reset, (state) => ({ counter: 0 }))
    );
    
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [CommonModule],
      template: `
        <div>Counτer : {{ (counterFinal$ | async)?.counter || 0 }}</div>
        <button id="plus" (click)="plus()">+</button>
        <button id="minus" (click)="minus()">-</button>
        <button id="reset" (click)="reset()">Reset</button>
      `,
    })
    export class App {
      //c!:number;
      counterFinal$: Observable<CounterState>; //= this.store.select("counter");
    
      constructor(private store: Store<{ counter: CounterState }>) {
        //this.store.select('counter').subscribe(data => this.c = data.counter);
        this.counterFinal$ = this.store.select('counter');
      }
    
      plus() {
        //this.count = this.count + 1;
        console.log('+');
        this.store.dispatch(plus());
      }
    
      minus() {
        //this.count = this.count - 1;
        console.log('-');
        this.store.dispatch(minus());
      }
    
      reset() {
        //this.count = 0;
        console.log('0');
        this.store.dispatch(reset());
      }
    }
    
    bootstrapApplication(App, {
      providers: [provideStore({ counter: CounterReducer })],
    });
    

    Stackblitz Demo