Explore the future of Web Scraping. Request a free invite to ScrapeCon 2024

A Simple Way to Load Data Before Page Load in Angular

Loading data before page load

You’ve probably already tried doing this in the ngOnInit function of the app.component, but realized your data needed to be loaded even sooner than that. You’ve also maybe tried implementing a resolver, but realized that they are a better fit in the context of individual routes. Here’s another approach to loading your data before page load that you might not know about yet: the APP_INITIALIZER.

Definition

Before diving into the code, let’s get a better understanding of what the APP_INITIALIZER is and how it works.

The APP_INITIALIZER token allows you to provide additional initialization functions to your application. An initialization function — as you might have already gathered from the name — is executed during app initialization. The return type of these functions must be either void, a Promise or an Observable. If a Promise or an Observable is returned from any of these functions, the application is only initialized after they are completed.

In simpler terms, I like to think of it as defining a “boot up” stage where you can make sure all the core data that your app needs in order to function properly is loaded before the user can start interacting with it.

Here are a few examples of what you could load in an initialization function:

  • translations
  • authenticated user data
  • configuration data

Example

For the sake of simplicity, let’s take the example of loading the currently authenticated user’s data.

Most web apps display the current user’s profile picture and name in the top-right corner of the screen so let’s implement something similar. I’m going to use a fresh Angular install (14.1) and Bootstrap 5 for the CSS:

ng new angular-app-initializer --routing --style=scss
cd angular-app-initializer
npm install --save bootstrap

Now open your styles.scss and import Bootstrap:

// src/styles.scss
@import '~bootstrap/scss/bootstrap';

If you’re using a different version of Angular, some imports and syntax might differ so please be mindful of that if you’re following along with the code.

Laying Out The Foundation

Let’s start by creating a service responsible for providing the current user’s data which for now is going to look something like this:

// src/app/service/user.service.ts
import { _Injectable_  } from '@angular/core';


@Injectable({
  providedIn: 'root'
})
export class UserService {
  private _data: IUserData = {
    name: 'guest',
    avatar: 'assets/no-avatar.png' _// grab a random avatar image and put it in your assets folder_
  }

  get data(): IUserData {
    return this._data;
  }
}

export interface IUserData {
  name: string,
  avatar: string
}

This will be the default value of our data until we make a request to fetch the actual current user data.

Let’s make use of this data and display it in the top-right corner of the screen. To keep it simple, we’re going to do this directly in the AppComponent.

First, we inject our UserService:

// src/app/app.component.ts
import { Component } from "@angular/core";
import { UserService } from "./service/user.service";

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.scss"],
})
export class AppComponent {
  constructor(public userService: UserService) {}
}

And then for the HTML:

<!-- // src/app/app.component.html -->
<nav class="navbar navbar-light bg-light">
  <div class="container">
    <a class="navbar-brand">MyApp</a>
    <div class="d-flex align-items-center">
      <p class="mb-0">{{userService.data.name}}</p>
      <img
        src="{{userService.data.avatar}}"
        class="ms-2 rounded-circle"
        style="width: 60px"
      />
    </div>
  </div>
</nav>
<main class="container py-4">
  <h1>Dashboard</h1>
  <p>Dashboard content goes here...</p>
</main>

I highlighted the important part in which we actually display the name and the profile picture of the user.

Now that we laid out the foundation, all that’s left to do is to implement a function that fetches the actual user data and then see when and where we’re going to call it.

I’m not going to use an actual backend service for this, but instead create a JSON file in our assets folder where we’re going to hardcode the data:

// src/assets/user.json
{
  "name": "Dragos Becsan",
  "avatar": "assets/avatar.jpg" // grab another avatar image and put it in your assets folder - remove this comment
}

We’re going to define the function responsible for fetching the data inside the UserService , like so:

// src/app/service/user.service.ts
import { _Injectable_ } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
import { delay, tap } from "rxjs/operators";

@Injectable({
  providedIn: "root",
})
export class UserService {
  private _data: IUserData = {
    name: "guest",
    avatar: "assets/no-avatar.png",
  };

  constructor(private http: HttpClient) {}

  public loadData(): Observable<IUserData> {
    return this.http.get<IUserData>("./assets/user.json").pipe(
      delay(1000), // to mimic an actual HTTP request sent to a backend service
      tap((response: IUserData) => (this._data = response))
    );
  }

  get data(): IUserData {
    return this._data;
  }
}

export interface IUserData {
  name: string;
  avatar: string;
}

Don’t forget to import the HttpClientModule in the AppModule .

Implementing The APP_INITIALIZER

As we learned in the definition part, the APP_INITIALIZER let’s us define additional initialization functions, so let’s define one now in a separate file.

_ / src / app / initializer / app.initializer.ts;
import { Observable } from "rxjs";
import { IUserData, UserService } from "../service/user.service";

export function initializeAppFactory(
  userService: UserService
): () => Observable<IUserData> {
  return () => userService.loadData();
}

You can name the file and the function whatever you want. I mostly stick with a more generic name since I usually load everything that I need in a single function using a forkJoin . If you prefer doing it differently, you can, for example, name this one loadUserDataFactory or something similar (since that’s the only thing that it does) and then create separate functions for loading any other data that you might need.

Now the only thing left to do is to flag this function as an APP_INITIALIZER so Angular knows to execute it during app initialization. To do this, we need to add the following provider to the providers array in our AppModule:

// src/app/app.module.ts_...providers: [
  {
    provide: APP_INITIALIZER,
    useFactory: initializeAppFactory,
    deps: [UserService],
    multi: true
  }
]
// ,...

And that’s it. If you refresh the page now, you should see a blank page for roughly 1 second (because of that delay we added when fetching the JSON file) after which the page will load with the actual user name and avatar (the ones specified in the user.json file) displayed in the top-right corner.

Things To Consider

  • Probably the most important thing to note is that if the Observable returned from any initializer function errors, the app will no longer be initialized. In our example, you can see this in action by renaming or temporarily removing the user.json file which will cause the Observable to fail with a 404 error. Consequently, you’re going to be stuck on that initial blank page.

    To stop this from happening, always make sure you catch any potential errors by using the catchError operator and either provide default values for your data or redirect the user to specific error pages where you can provide them with details of what went wrong and how they can move forward. In our example, redirecting to an error page might look something like this — if you want to try this out, don’t forget to add the _Router_as a dependency by updating the _deps_ key of the provider in AppModule and create the new page and its route:

// src/app/initializer/app.initializer.ts
import { from, Observable } from "rxjs";
import { UserService } from "../service/user.service";
import { catchError } from "rxjs/operators";
import { Router } from "@angular/router";

export function initializeAppFactory(
  userService: UserService,
  router: Router
): () => Observable<any> {
  return () =>
    userService
      .loadData()
      .pipe(catchError(() => from(router.navigate(["/error"]))));
}
  • The blank page that you’re seeing for 1 second is actually the index.html . The reason this happens is that since the app is not initialized until the Observable completes, the <app-root> element is not populated and so you’re seeing nothing. What I usually do is add a loading image/text as a child of the <app-root> element. Any content that you put inside the<app-root> will get overwritten when the app finishes the initialization. I’ll give you an example that you can try out below. If you want to play around with it, consider increasing the delay of the Observable in the initializeAppFactory function.
<!-- // src/index.html ... -->
<app-root>
  <div class="vh-100 d-flex align-items-center justify-content-center">
    <p class="h2">Loading...</p>
  </div> </app-root
>...
  • Get comfortable using RxJS (unless you’re using Promises) as more often than not you’re going to need to play around with a bunch of RxJS functions and operators to get to the right outcome. forkJoin , iif , switchMap , map , catchError , tap are the ones that I use frequently in cases like this. For instance, in our example we only took care of the case when a user is logged in, but what if the user is in fact a guest. In that case, we might want to stick with the default data that we defined. One way to do this in a single stream would look something like this — keep in mind that this is just an example and it won’t work out of the box in the current project since we haven’t defined any _authService_ together:
// src/app/service/user.service.ts

// ...

public loadData(): Observable<IUserData> {
  return iif(
    () => this.authService.isLoggedIn,
    this.http.get<IUserData>('./assets/user.json'),
    of({name: 'guest', avatar: 'assets/no-avatar.png'})
  ).pipe(
    tap((output: IUserData) => this._data = output),
  );
}
// ...

Resources:

Thank you very much for following along until the end! 🎉

If you found this tutorial useful, please follow me for more content like this.

I’m open to suggestions if there’s anything, in particular, you’d like me to cover from the list of technologies in my bio so please feel welcome to drop a comment if you have any.




Continue Learning