Search code examples
javascriptangular6ngxs

i can't read my state once i load my component, state is properly changing


i'm currently trying to understand how NGXS works, so i was able to setup the state and action but when i call my service and i try to put the state into a variable i get an observable.

attached is the code and what i get from the console, i'm trying to get a list of items when my application starts so basically i have 2 states the clients array which will store the response from the service and a loading state which will change to false if i get a response, this is the first time i'm trying to handle this concepts so thanks in advance for the help

  import { State, Action, StateContext, Selector } from "@ngxs/store";
  import { Client } from "../models/client-model";
  import { GetAllClients } from "../actions/client.actions";
  import { ClientService } from "../services/client.service";
  import { tap } from 'rxjs/operators';

  export class ClientStateModel {
    clients: Client[];
    loading: boolean;
  }

  @State<ClientStateModel>({
    name: "clients",
    defaults: {
      clients: [],
      loading: true
    }
  })
  export class ClientState {
    constructor(private _clientService: ClientService) {}

    @Action(GetAllClients)
    getClients(ctx: StateContext<ClientStateModel>, action: GetAllClients){
      return this._clientService.getClients().pipe(tap(clientsList => {
        const state = ctx.getState();
        ctx.setState({
          ...state,
          clients: clientsList,
          loading: false
        });
      }))
    }
  }

this is the service

  import { Injectable } from '@angular/core';
  import { HttpClient, HttpHeaders } from '@angular/common/http';
  import { Observable } from 'rxjs';
  import { environment } from '../../environments/environment';

  @Injectable()
  export class ClientService {
    public url: string;

    constructor(
      private _http: HttpClient
    ) {
      this.url = environment.apiURL;
    }

    getClients(): Observable<any> {
      const headers = new HttpHeaders({'Content-Type': 'application/json'});
      return this._http.get(`${this.url}get-clients`,{headers: headers});
    }
  }

and this will be my attempt to consume the state

  import { Component, OnInit } from "@angular/core";
  import { Client } from "./models/client-model";
  import { Router } from "@angular/router";
  import { Store } from "@ngxs/store";
  import { GetAllClients } from "./actions/client.actions";
  import { Observable } from "rxjs";

  @Component({
    selector: "app-root",
    templateUrl: "./app.component.html",
    styleUrls: ["./app.component.css"]
  })
  export class AppComponent {
    title = "Managed Services Dashboard";
    Clients: Observable<Client>;
    isLoading;

    constructor(private store: Store, private _router: Router) {}

    ngOnInit(): void {
      this.getClients();
    }

    getClients() {
      this.store.dispatch(new GetAllClients()).subscribe(result => {
        this.Clients = this.store.select(state => state.clients.clients);
      })
    }

    goToSelectedClient(client) {
      console.log(client);
      this._router.navigate(["client-details"]);
    }
  }

this is what i get in the console.

console


Solution

  • Ok, so first on the service, if you're getting a list of clients back, you can make the return type more specific:

    getClients(): Observable<Client[]> {
      const headers = new HttpHeaders({'Content-Type': 'application/json'});
      return this._http.get(`${this.url}get-clients`,{headers: headers});
    }
    

    Next, the action that gets the clients doesn't need an action parameter since you aren't passing any data in it. Also it can use patchState like this:

    @Action(GetAllClients)
    getClients(ctx: StateContext<ClientStateModel>) {
        return _clientService.getClients().pipe(
            tap ( (clients: Client[]) => {
                ctx.patchState({ clients });
            }),
        );
    }
    

    In your component, you can use @Select to get an observable on client state changes:

    @Component({
        selector: "app-root",
        templateUrl: "./app.component.html",
        styleUrls: ["./app.component.css"]
    })
    export class AppComponent {
        @Select(ClientState) clientState$: Observable<ClientStateModel>;
        title = "Managed Services Dashboard";
        isLoading;
        .
        .
        .
        getClients() {
          this.store.dispatch(new GetAllClients());
        }
    

    elsewhere in the component, or in the template, you can use the clientState$ observable:

        <div class="list-group">
          <a
            class="list-group-item"
            style="cursor: pointer"
            *ngFor="let client of (clientState$ | async).clients">
            {{ client.name }}
          </a>
        </div>
    

    You can also subscribe to the observable to handle client list changes in the code. You'll notice I removed the subscribe to the dispatch. That is specified to return a void, but can be used to determine if an error happened in your action handler (say an HTTP error) - that you would want to handle in the component.