21

I have an Angular 2 service that needs to do async work when it is initalized, and should not be usable before this initialization has been completed.

@Injectable()
export class Api {
    private user;
    private storage;

    constructor(private http: Http) {
        this.storage = LocalStorage;
        this.storage.get('user').then(json => {
            if (json !== "") {
                this.user = JSON.parse(json);
            }
        });        
    }

    // one of many methods
    public getSomethingFromServer() {
        // make a http request that depends on this.user
    }
}

As it currently stands, this service is initialized, and returned immediately to any component that uses it. That component then calls getSomethingFromServer() in its ngOnInit, but at that point Api.user is not initialized, and therefore the wrong request is sent.

The lifecycle hooks (OnInit, OnActivate, etc) does not work with services, only components and directives, so I cannot use those.

Storing the Promise from get() call would require all the different methods that depend on user to wait for it, causing a lot of code duplication.

What is the recommended way to do async initialization of services in Angular 2?

1
  • using APP_INITIALIZER may be an approach in some scenarios e.g. if you want the init to be called just once, see an example here stackoverflow.com/questions/49686406/… I realise your q is already answered but this may be useful for future reference Commented Jun 4, 2019 at 22:29

2 Answers 2

6

After working with Thierry's answer for a bit, I discovered that it would only work once, but it did set me on the right path. I had to instead store the promise of the user, and create a new observable which is then flatMap-ed.

@Injectable()
export class Api {
  private userPromise: Promise<User>;

  constructor(private http: Http) {
    this.userPromise = LocalStorage.get('user').then(json => {
      if (json !== "") {
        return JSON.parse(json);
      }
      return null;
    });        
  }

  public getSomethingFromServer() {
      return Observable.fromPromise(this.userPromise).flatMap((user) => {
        return this.http.get(...).map(...);
      });
    }
  }
}

This ensures that the flatMap function gets the user every time it is called, instead of just the first time, as in Thierry's answer.

Sign up to request clarification or add additional context in comments.

2 Comments

What if user exist in LocalStorage ? Aren't you going to fetch it from http every time ?
@MadHollander the OP just wanted to wait for the bootstrap of the service before executing the http call - the http call isn't going to get the user, its just getting 'something from server' which requires the user being resolved beforehand
5

You could leverage an observable to do that with the flatMap operator. If the user isn't there, you could wait for it and then chain the target request.

Here is a sample:

@Injectable()
export class Api {
  private user;
  private storage;
  private userInitialized = new Subject();

  constructor(private http: Http) {
    this.storage = LocalStorage;
    this.storage.get('user').then(json => {
      if (json !== "") {
        this.user = JSON.parse(json);
        this.userInitialized.next(this.user);
      }
    });        
  }

  // one of many methods
  public getSomethingFromServer(): Observable<...> {
    // make a http request that depends on this.user
    if (this.user) {
      return this.http.get(...).map(...);
    } else {
      return this.userInitialized.flatMap((user) => {
        return this.http.get(...).map(...);
      });
    }
  }
}

4 Comments

The thought has occured to me. However, I have many of these getSomethingFromServer, and this would require all of them to do this little dance. Is there a way to have something like an async factory pattern for this?
The more I think about this, the better your solution sounds. I am having a go at implementing it, but I'm having a bit of trouble. Where is Subject from? Is it Rx.Subject? That doesn't seem to have an emit method. Generally, what imports do I need for this?
Yes you're right! There is a typo: it's next instead of emit (emit is when using the EventEmitter class). Subject is from rxjs and can be imported this way: import {Subject} from 'rxjs/Subject'; or import {Subject} from 'rxjs/Rx';.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.