Lots of times, there is a need to load data using Promises from API before the page loads or Class initialization.
To achieve this I see many of my fellow devs use async
on ngOnInit
so they can await
on data fetch API method inside ngOnInit
async ngOnit {
this.movies = await this.service.getMovies();
}
But if you see carefully there is no one awaiting ngOnInit
and you can’t await ngOnInit
even if you wanted to.
It will run the async function but it WILL NOT await for it to complete, it just allows you to use the await
keyword, but it is not a aysnc
function, despite having the async
keyword.
It looks kind of awkward and somewhat hard to comprehend, doesn’t seem conventional.
Ideally, the way is to use route resolvers so that data is loaded before the route finishes navigating and I can know the data is available before the view is ever loaded.
A lot of Stack Overflow answers point to a more readable way of doing it without adding async
keyword to ngOnInit
but the solution behaves the same.
ngOnInit() {
(async () => {
this.movies = await this.service.getMovies();
});
}
Also there is a interesting take by
on Forcing Angular to Wait on Your Async Function a unique way to await
components constructor
.
And this is not just for Angular components. Imagine a simple class that loads some data and also has getters and setters for that data.
class MovieService {
private movies: Movie[];
constructor() {
this.loadMovies();
}
getMovieById(id: number) {
return this.movies.find((movie) => movie.id === id);
}
getAllMovies() {
return this.movies;
}
private async loadMovies() {
this.movies = await MovieStore.getTodos();
}
}
In above example, the data will start loading immediately when the instance of the class is created and the constructor can’t be blocked until the data is fetched completely. So if getters are called while the request is still pending, it might end up in default empty or undefined
movies object.
So you can only hope that the data will be loaded by the time any getters are called. Turn around for this is to add some lazy loading and make accessors await the load request something like below.
async getMoviesById(id: number){
if(!this.movies){
await this.loadMovies();
}
return this.movies.find(movie => movie.id === id);
}
Looks good right, but this code now becomes a duplicate code and the load request might be fired more than once only if we could find a way to abstract all this wiring with something else.
TypeScript Decorators to the rescue 🎉
A Decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Short and crisp, a decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it. A good way to abstract the wiring would be using decorators somewhere we can wait for called all methods which are to be run and completed first, and then run the ones which are dependent on those. Something like below: Here’s how to make these decorators work. You can add each decorator to as many methods as you’d like.
Code explanation for those who are new to Decorators
To understand the init/waitForInit decorator code we first must understand how the decorators are evaluated.
TypeScript documentation on Decorators here does a wonderfull job of explaing how decorators are evaluated, I will recommend checking it.
Let’s take the below example to understand the evaluation:
Same as above, all our init
decorated methods would be evaluated first, where in the movies service case, loadMovies
would be registered as one of the INIT_METHODS
and then when any one of the methods decorated with waitForInit
like getAllMovies
would be called then it would first run the loadMovies
Promise first and then run the corresponding calling function.
Wait 🤔 We were talking about 'async ngOnInit'
where is the code? 👊
Don’t worry, we got you covered. Here is how you can use the same init/waitForInit
decorator on ngOnInit
.
Here is the Stackblitz example demonstrating the said decorators on ngOnInit
.
https://angular-ivy-kq23e9.stackblitz.io
Concluding, we briefly discussed the implementation on pre-loading the data in any class or before template initialization in ngOnInit
in Angular using the TypeScript decorator.
References: