4

I would like an efficient way to store (and update) the current user details in the frontend, rather than making new HTTP GET requests to the backend whenever a new component is loaded.

I have added a UserService class which sets/updates a currentUser object when a method in the class is called.

import { Http, Response } from '@angular/http';
import { Injectable } from '@angular/core';

@Injectable()
export class UserService {

    public currentUser: any;

  constructor(private http: Http) { }

  updateCurrentUser() {
    this.http.get('api/login/currentUser').subscribe(data => {
        this.currentUser = data;
    });
  }

}

I have found this method to cause a race condition problems. For example, an error occurs if the currentUser.username is requested in the profile component but the service class hasn't completed the request yet.

I've also tried checking if the currentUser is undefined and then calling the updateCurrentUser() but it doesn't seem to work.

if(this.userService.currentUser === undefined){
    this.userService.updateCurrentUser();
} 


Update:The error message shown in the browser console enter image description here


Update on HTML used:
I am using data binding to display the username (calling it directly from the UserService class). E.g.

<span class="username-block">{{userService.currentUser.username}}</span>

I have also tried assigning the currentUser object to a variable within the component that I trying to display the username but it has the same result.


I would appreciate any help/feedback.

ToDo
  • 754
  • 2
  • 17
  • 31
  • Store the user as a global variable once they log in: https://stackoverflow.com/questions/39552388/angular-2-universal-store-global-variables – DGK Oct 05 '17 at 19:27
  • Related question, https://stackoverflow.com/questions/40249629/how-do-i-force-a-refresh-on-an-observable-service-in-angular2 – Estus Flask Oct 05 '17 at 19:37
  • 1
    Store it in localstorage, you can access it directly or use angular 2 localstorage package – JSingh Oct 05 '17 at 19:42
  • It seems odd that you would get a race condition? If you access currentUser before its set you should just get a null value back. Maybe we need to see the code that causes the race condition? – DeborahK Oct 05 '17 at 19:56
  • Thank you for all your feedback. There seem to be many ways around this, I will research them all and determine which one is the best solution. – ToDo Oct 05 '17 at 20:58
  • This is an error in the html. If you could show the part of your html that uses `username`, we could provide a more specific solution. – DeborahK Oct 06 '17 at 00:07

2 Answers2

11

You can try the Elvis/existential operator (I think that's the right name) in the html. The question mark after currentUser says don't bother trying to evaluate username if currentUser is undefined.

Assuming your async code will eventually fill it in, the page should then display it.

<span class="username-block">{{userService.currentUser?.username}}</span>

Edit

I just noticed you are outputting userService.currentUser.username. You may want to return an observable from the service to the component so that the data is reactive. Something like,

updateCurrentUser() {
    return this.http.get('api/login/currentUser')
      .catch(error => this.handleError(error, 'currentUser'));
}

Then in the component,

private currentUser;

ngOnInit() {
  this.userService.updateCurrentUser()
    .take(1)
    .subscribe(data => { this.currentUser = data });
}

and the html now refers to the local component copy, initially undefined but eventually filled in by the subscription. Note, take(1) is to close the subscription and avoid memory leaks.

    <span class="username-block">{{currentUser?.username}}</span>

Edit #2

Apologies, your question asks about avoiding multiple HTTP calls.
In that case the service code should be

export class UserService {

  private currentUser: any; // private because only want to access through getCurrentUser()

  constructor(private http: Http) { }

  getCurrentUser() {
    return this.currentUser
      ? Observable.of(this.currentUser) // wrap cached value for consistent return value
      : this.http.get('api/login/currentUser')
          .do(data => { this.currentUser = data }) // cache it for next call
          .catch(error => this.handleError(error, 'currentUser'));
  }
Richard Matsen
  • 20,671
  • 3
  • 43
  • 77
9

Are you using routing? If so, you could set up a route resolver on your first route that needs the user data. The resolver then always waits to display the view until the resolver data is retrieved.

For example, here is one of my route resolvers. Notice that it calls a service to retrieve the data.

import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

import { Observable } from 'rxjs/Observable';

import { IMovie } from './movie';
import { MovieService } from './movie.service';

@Injectable()
export class MovieResolver implements Resolve<IMovie> {

    constructor(private movieService: MovieService) { }

    resolve(route: ActivatedRouteSnapshot,
            state: RouterStateSnapshot): Observable<IMovie> {
        const id = route.paramMap.get('id');
        return this.movieService.getMovie(+id);
    }
}

The resolver is then added to the appropriate route, something like this:

  {
    path: ':id',
    resolve: { movie: MovieResolver },
    component: MovieDetailComponent
  },

That route won't be displayed then until the data is retrieved.

DeborahK
  • 57,520
  • 12
  • 104
  • 129
  • 2
    This solution, unlike the Elvis / existential suggestion above, allows us to avoid exposing any internalish class methods to the component HTML & seems generally more de-coupled while accomplishing the intended effect. – Sean Halls Jul 28 '19 at 22:20