0

Here is what I want to achieve: There is a component that has a UserService in it. After log in I want to call that service to retrieve user data from backend and store it in a service field. Then after I reload main page I want that service to do not call backend again, just return previously stored data. Additionally I want some divs to do not load before a data is loaded. How can I achieve that? Should I make getAllUSerInfoNormal() method to return observable, if its possible how should I do that? Or there is some other way?

Here is what I have got right now:

Service class:

@Injectable()
export class AccountService {

    userAccount : UserAccount;
    constructor(private _httpClient: HttpClient) {
    }


     getAllUSerInfoNormal(): UserAccount {
    if (!this.userAccount) {
        this._httpClient.get<UserAccount>(Constants.apiRoot + 'account/accountInfo').subscribe(user => {
            this.userAccount = user;
            return this.userAccount
        })
    } else {
        return this.userAccount;
    }
}



Component class:

@Component({
  selector: 'app-main-view',
  templateUrl: './main-view.component.html',
  styleUrls: ['./main-view.component.scss']
})
export class MainViewComponent implements OnInit {

  private account: UserAccount;
  private isDataLoaded = false;

  constructor(private _router: Router,
              private _accountService: AccountService) {
    setInterval(() => {
      this.updateResources();
    }, 1);
  }

  ngOnInit() {
    this._accountService.getAllUSerInfoNormal();
  }

  updateResources(){

  }
}



Component template:

<div class="ingame-menu">
    <div *ngIf="isDataLoaded">
        <div class="resources">
            <div class="resource"><img src="../../../assets/images/resources/gold.png" title="Gold"/>
                <br/> {{account.gold}}</div>
            <div class="resource"><img src="../../../assets/images/resources/wood.png" title="Wood"/>
                <br/> {{account.wood}}</div>
            <div class="resource"><img src="../../../assets/images/resources/stone.png" title="Stone"/>
                <br/> {{account.stone}}</div>
            <div class="resource"><img src="../../../assets/images/resources/meat.png" title="Meat"/>
                <br/> {{account.people}} </div>
        </div>
    </div>
    <div class="ingame-nav">
        <ul>
            <li><a routerLink="overview">Overview</a></li>
            <li><a routerLink="population">Population</a></li>
            <li><a routerLink="resources">Resources</a></li>
            <li><a routerLink="research">Research</a></li>
            <li><a routerLink="buildings">Buildings</a></li>
            <li><a routerLink="barracks">Barracks</a></li>
            <li><a routerLink="tavern">Tavern</a></li>
            <li><a routerLink="defence">Defence</a></li>
        </ul>
    </div>
    <div id="content">
        <router-outlet></router-outlet>
    </div>
</div>

2 Answers2

1

You can save the loaded user in the lcoalStorage after the first load and then just read it on the following accesses.

This is a simple solution to learn. But in a real application scenario you shouldn't have "infinite time" cache like this. And must have a way to update the cache sometimes.

Change the service class to :

@Injectable()
export class AccountService {

    get userAccount() {
      return JSON.parse(localStorage.getItem('user'));
    };

    constructor(private _httpClient: HttpClient) {
    }


    getAllUSerInfoNormal(forceBanckendCall: boolean): Observable<UserAccount> {
        if (this.userAccount && !forceBackendCall) {
           return of(this.userAccount);
        }

        return this._httpClient.get<UserAccount>(Constants.apiRoot + 'account/accountInfo')
           .pipe(
             tap(user => localStorage.setItem('user', JSON.stringify(user)))
           );

    }

then the component class to:

@Component({
  selector: 'app-main-view',
  templateUrl: './main-view.component.html',
  styleUrls: ['./main-view.component.scss']
})
export class MainViewComponent implements OnInit {

  private account: UserAccount;
  private isDataLoaded = false;

  constructor(private _router: Router,
              private _accountService: AccountService) {
    setInterval(() => {
      this.updateResources();
    }, 1);
  }

  ngOnInit() {
    this._accountService.getAllUSerInfoNormal().subscribe(account => this.userAccount = account);
  }

  updateResources(){

  }
}

then in your component template place a *ngIf="userAccount" in the elements that you want to be loaded just after the user is present.

Eduardo Junior
  • 362
  • 1
  • 10
0
  1. You cannot return the value from the subscription synchronously like you're attempting. The variable from an observable is emitted asynchronously. Please see here for more info on async data. In short, the value is available only inside the subscription.

  2. There are multiple ways to cache a notification from an observable. One of the quickest way is to use a multicast observable (like Subject) in the service to hold the data. I'll illustrate using ReplaySubject with buffer 1 since it can hold on to the current value.

Service

import { ReplaySubject } from 'rxjs';

@Injectable()
export class AccountService {
  userAccountSource: ReplaySubject<UserAccount> = new ReplaySubject<UserAccount>(1);
  userAccount$ = this.userAccountSource.asObservable();
  
  constructor(private _httpClient: HttpClient) {
    this.getAllUSerInfoNormal().subscribe();  // <-- push to cache on app start
  }

  getAllUSerInfoNormal(): Observable<UserAccount> {
    return this._httpClient.get<UserAccount>(Constants.apiRoot + 'account/accountInfo').pipe(
      tap({
        next: user => this.userAccountSource.next(user),
        error: error => { }  // <-- handle error
      })
    );
  }
}
  1. Now if the emission is not needed in the component controller, you can straight up skip the subscription in it and using the Angular async pipe in the template. Besides keeping the code tidy, it'll handle the potential leak issue of an open subscription.

Controller

@Component({...})
export class MainViewComponent implements OnInit {
  private isDataLoaded = false;

  constructor(
    private _router: Router,
    private _accountService: AccountService
  ) {
    setInterval(() => {
      this.updateResources();
    }, 1);
  }

  ngOnInit() {
  }

  updateResources() {
  }
}

Template

<div class="ingame-menu">
  <div *ngIf="(_accountService.userAccount$ | async) as account">
    <div class="resources">
      <div class="resource"><img src="../../../assets/images/resources/gold.png" title="Gold" />
        <br /> {{account.gold}}</div>
      <div class="resource"><img src="../../../assets/images/resources/wood.png" title="Wood" />
        <br /> {{account.wood}}</div>
      <div class="resource"><img src="../../../assets/images/resources/stone.png" title="Stone" />
        <br /> {{account.stone}}</div>
      <div class="resource"><img src="../../../assets/images/resources/meat.png" title="Meat" />
        <br /> {{account.people}} </div>
    </div>
  </div>
  <div class="ingame-nav">
    <ul>
      <li><a routerLink="overview">Overview</a></li>
      <li><a routerLink="population">Population</a></li>
      <li><a routerLink="resources">Resources</a></li>
      <li><a routerLink="research">Research</a></li>
      <li><a routerLink="buildings">Buildings</a></li>
      <li><a routerLink="barracks">Barracks</a></li>
      <li><a routerLink="tavern">Tavern</a></li>
      <li><a routerLink="defence">Defence</a></li>
    </ul>
  </div>
  <div id="content">
    <router-outlet></router-outlet>
  </div>
</div>
  1. As said, there are multiple other ways to cache the data from an observable. You can start from here and here.

  2. Unrelated to the issue, I find the usage of setInterval() with argument 1 very inelegant. Do you wish to poll the end-point regularly to keep the data updated? If so you could look into RxJS timer or interval function. They let you poll with a single subscription instead of opening a new subscription every n seconds.

ruth
  • 29,535
  • 4
  • 30
  • 57
  • This solution doesn't allow him to update the page and don't go to backend again as he said `Then after I reload main page I want that service to do not call backend again, just return previously stored data` – Eduardo Junior Jan 28 '21 at 13:12
  • @EduardoJunior: OP didn't explain what kind of `reload`. If it's web page reload, then the value could be cached in browser storage. If the are rerouting to the main page within the app, backend will not be called again. The data from the `ReplaySubject` will be used instead. – ruth Jan 28 '21 at 13:20
  • you're right. Just interpreted his `reload` as a F5 hehe. – Eduardo Junior Jan 28 '21 at 13:21