Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 2/4 - Storing the Current User Object Details in front-end (rather than making continuous HTTP requests to back-end)

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.

like image 452
ToDo Avatar asked Nov 29 '22 22:11

ToDo


2 Answers

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'));
  }
like image 85
Richard Matsen Avatar answered Dec 04 '22 07:12

Richard Matsen


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.

like image 28
DeborahK Avatar answered Dec 04 '22 07:12

DeborahK