DEV Community

Cover image for Understanding Angular's APP_INITIALIZER with Practical Use-Cases
Ayoub Khial
Ayoub Khial

Posted on • Updated on

Understanding Angular's APP_INITIALIZER with Practical Use-Cases

Introduction

There are some specific tasks that need to be done before an Angular application is fully initialized. This is where Angular's APP_INITIALIZER will help. It plays a vital role in initialization for any app, as it runs all of the functions that are necessary to set everything up before the app starts, hence saving the developer lots of hassle.

One should know how to use the initializer properly to make an Angular application more error-resilient and reliable. It is a capability with huge growing potential and, hence, new opportunities for your projects. I will describe in this article what APP_INITIALIZER is, how to set it up, practical use cases, frequent pitfalls, and best practices.

But to understand the real value of it, let's define it and then cover basic usage.

What is APP_INITIALIZER

APP_INITIALIZER is an Angular injection token that allows the execution of one or more functions during initialization. This is very helpful in things that need to happen before the app is usable, such as loading configuration settings, initializing services, or prefetching critical data.

Angular injection token is a special kind of provider that Angular uses to inject dependencies into your application. Injection tokens allow you to define and inject custom objects, values, or services in your app's dependency injection system.

During initialization, Angular will search for all providers associated with this initializer token. Every provider may return either a Promise or an Observable. Angular will wait for all these functions to resolve before bootstrapping the application so it can be sure that all setup steps are finished; hence, the application is fully prepared for user interaction.

Here's a basic example of how to configure it in your Angular application:

import { APP_INITIALIZER, ApplicationConfig } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    // Any other providers, such as router, http, etc.
    {
      provide: APP_INITIALIZER,
      useFactory: initializeApp,
      multi: true
    }
  ]
};

export function initializeApp() {
  return (): Promise<void> => {
    return new Promise<void>((resolve) => {
      console.log('Initialization complete');
      setTimeout(() => {
        resolve();
      }, 5000);  // Simulate a 5-second initialization task
    });
  };
}
Enter fullscreen mode Exit fullscreen mode

Here, initializeApp simulates an asynchronous task with a setTimeout function that resolves in five seconds. The APP_INITIALIZER provider will be set up with this function, and Angular will not start the application until it resolves.

To further put into perspective where and how it fits in with Angular initialization, consider the following flowchart:

APP_INITILIZER in Angular Lifecycle

This flowchart shows how the functions of APP_INITIALIZER are executed while Angular is starting up, thus ensuring that everything is done before the application is fully operational.

Now that we know the basics let's see how to configure it in a real Angular project with practical examples.

Setting up the initializer

To effectively set up APP_INITIALIZER, you must follow a clear process that integrates seamlessly into your Angular application.

First, create the initialization function. This function should handle all setup tasks before the application bootstraps. This could include loading configuration data or even initializing services; whatever setup type your application may require should go into this function. Returning either a Promise or an Observable allows Angular to wait for these tasks to complete, thereby bootstrapping the application.

For example, you want to load some settings from a server before the application starts. You may then create a function such as this one:

export function loadSettings(): Promise<any> {
  return new Promise((resolve, reject) => {
    // Simulate loading configuration from a server
    setTimeout(() => {
      console.log('Settings loaded');
      resolve();
    }, 2000);
  });
}
Enter fullscreen mode Exit fullscreen mode

The above function would simulate an asynchronous task —like fetching settings from a server— by resolving it after a short delay. The returned promise of this function ensures that Angular will not initialize the application until this task is resolved.

Now, it's time to wire up this initialization function into your Angular configuration. You can do this via the initializer token by letting Angular know it should execute the function passed during its initialization phase. Here is how you might set things up in your Angular config:

import { APP_INITIALIZER, ApplicationConfig } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: loadSettings,
      multi: true
    }
  ]
};
Enter fullscreen mode Exit fullscreen mode

It defines a setup that sets up an initialization function executed by the useFactory property. An important thing is the multi: true property because it allows several initialization functions to be registered under the token. It will be very vital in complex applications where many tasks have to be done before starting the app.

In the case of services such as HttpClient, you may want your initializer function to perform them. You would typically use a factory function to inject any dependencies into the initializer function. Suppose that you want to set up a certain configuration file by loading it from a server via HttpClient; the setup would look something like this:

import { APP_INITIALIZER, ApplicationConfig } from '@angular/core';

export function loadConfig(http: HttpClient): () => Promise<any> {
  return (): Promise<any> => {
    return http.get('/api/config').toPromise().then(data => {
      localStorage.setItem('config', data)
    });
  };
}

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: loadConfig,
      deps: [HttpClient],
      multi: true
    }
  ]
};
Enter fullscreen mode Exit fullscreen mode

In this example, a function loadConfig uses HttpClient to fetch configuration data from an API endpoint. By listing HttpClient in the deps array, Angular will automatically inject it into the loadConfig factory function when it runs.

Suppose your app needs to run several initializers. You can register them all under the APP_INITIALIZER token and make sure each has multi: true set. Then Angular will run all of the necessary initializers before bootstrapping the app:

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: loadSettings,
      multi: true
    },
    {
      provide: APP_INITIALIZER,
      useFactory: loadConfig,
      deps: [HttpClient],
      multi: true
    }
  ]
};
Enter fullscreen mode Exit fullscreen mode

This configuration ensures the execution of loadSettings and loadConfig during the app's initialization, making it easier to handle complicated setup procedures.

The providers run in the same order you declare them; In this case loadSettings runs before loadConfig.

Proper configuration of APP_INITIALIZER will make sure that all these critical actions are done before your Angular application has even started. Such setup makes an app more reliable and improves performance because all configurations and services are ready at its start.

Now that we have covered how to set it up let's dive into some real-world use cases on how it can help resolve actual problems in Angular apps.

Practical use cases of APP_INITIALIZER

APP_INITIALIZER allows Angular apps to run essential startup tasks before becoming fully operational. This ensures that configurations, services, and other critical processes are set up properly, enhancing app reliability and user experience. Below are some practical use cases that prove its utility:

Use case 1: loading configuration settings from a server

Many applications need to load up configuration settings from a remote server prior to an application beginning; this ensures that all its components are correctly set up before it goes on to render. For example, you can fetch environment-specific settings or fetch feature toggles, which impact how your app behaves.

import { HttpClient } from '@angular/common/http';

export function loadAppConfig(configService: ConfigService): () => Promise<any> {
  return (): Promise<any> => {
    return configService.get().toPromise().then(config => {
      localStorage.setItem('config', config);
    });
  };
}

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: loadAppConfig,
      deps: [ConfigService],
      multi: true
    }
  ]
};
Enter fullscreen mode Exit fullscreen mode

In this example, loadAppConfig fetches configuration data from the server and stores them locally. This example can be used so all components have these settings available before the app initializes.

Use case 2: initializing third-party services

A lot of applications engage third-party services. Among these are authentication providers, analytic tools, or external apis. The majority of these third-party services require initialization before the application is started.

import { AuthService } from './auth.service';

export function initializeAuth(authService: AuthService): () => Promise<any> {
  return (): Promise<any> => {
    return authService.initialize();
  };
}

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: initializeAuth,
      deps: [AuthService],
      multi: true
    }
  ]
};
Enter fullscreen mode Exit fullscreen mode

In this case, the initialization of AuthService takes place at app startup. That sets up the authentication mechanisms to be ready for use by the time the app starts. It may become more critical in apps where user authentication is paramount.

Use case 3: enable/disable routes based on user role

Another strong use case is the dynamic configuration of routes, depending on the user's role or permissions. This approach makes sure that all the navigation in the application is set up according to the user's role before the user has even seen the interface.

import { DataService } from './data.service';

export function initializeRouting(
  router: Router,
  authService: AuthService,
): () => Promise<void> {
  return () =>
    new Promise((resolve) => {
      if (authService.isAdmin()) {
        router.resetConfig([
          ...routes,
          {
            path: 'dashboard',
            component: DashboardComponent,
          },
        ]);
      }
      resolve();
    });
}

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: initializeRouting,
      deps: [RouterAuthService],
      multi: true
    }
  ]
};
Enter fullscreen mode Exit fullscreen mode

In this example, an application will dynamically change its route configuration depending on a user's role. Additional routes would be activated by being an admin, like an admin dashboard. A good approach to make sure that only the relevant routes are available for use by the user in this case is when the roles are very distinct. It can significantly enhance the security and user experience of the application.

Implementing these use cases with an initializer will greatly improve the performance, reliability, and user experience of your application by ensuring that all critical setup tasks are treated before the app is fully operational. That said, while this is a powerful tool, there are also possible pitfalls and best practices to avoid common mistakes. In the next section, we will discuss them.

Common pitfalls and best practices

Although APP_INITIALIZER is quite a powerful tool in Angular, there are challenges that it brings along with itself. Its misuse can also be responsible for performance and reliability problems in your application. Let's see some common pitfalls and the best practices that will save us from them.

Common pitfalls

While the initializer is a powerful feature in Angular, it comes with potential challenges that can impact your application's performance and reliability. Understanding these pitfalls is crucial to avoid common mistakes and ensure that your app initializes smoothly and efficiently.

  • Blocking initialization indefinitely: This major pitfall occurs when an application's initialization is blocked indefinitely. In cases where an initializer function does not resolve—either due to an error or by hanging—the application will be stuck in an endless loading state. You can ensure that this never happens by guaranteeing all your initializer functions handle any potential errors gracefully.
export function loadConfig(http: HttpClient): () => Promise<any> {
  return (): Promise<any> => {
    return http.get('/api/config').toPromise()
      .then(config => console.log('Config loaded:', config))
      .catch(error => {
        console.error('Error loading config:', error);
        return Promise.resolve(); // Resolves even if there's an error
      });
  };
}
Enter fullscreen mode Exit fullscreen mode
  • Long initialization time: are another common problem. Lengthy initialization tasks sometimes slow down the start-up of your application to a great deal, frustrating users who expect quick responses. To mitigate this, it's important to optimize the tasks performed in the initializer functions. One could use lazy loading on non-critical data in regions or perhaps defer certain tasks until the app has bootstrapped. Since this approach loads important information first and non-essential tasks later, the app can start sooner.
export function initializeServices(service: SomeService): () => Promise<void> {
  return (): Promise<void> => {
    return service.initializeCriticalData()
      .then(() => service.deferNonCriticalData()); // Defer non-critical data loading
  };
}
Enter fullscreen mode Exit fullscreen mode
  • Dependencies not provided: This is another pitfall whereby one forgets to provide the necessary dependencies in the deps array. It could result in runtime errors because Angular cannot inject the required services into your initializer functions. This will involve checking that all the dependencies that are really required by your initializer functions are correctly mentioned in the deps array so that they will be correctly injected.
{
  provide: APP_INITIALIZER,
  useFactory: loadConfig,
  deps: [HttpClient], // Ensure HttpClient is provided
  multi: true
}
Enter fullscreen mode Exit fullscreen mode

Best practices

To make the most of the application initializer, it's important to follow certain best practices. These guidelines will help you avoid common issues, optimize initialization processes, and ensure your Angular application runs smoothly from the moment it starts.

  • Modular initializers: A complex initialization logic should be split into smaller, modular functions. This goes for all of your code, really. It makes maintenance a whole lot easier, and testing/debugging less painful.
export function loadSettings(): Promise<any> {
  return loadUserSettings().then(loadAppSettings);
}

export function loadConfig(http: HttpClient): () => Promise<any> {
  return (): Promise<any> => {
    return http.get('/api/config').toPromise()
      .then(config => console.log('Config loaded:', config))
      .catch(error => {
        console.error('Error loading config:', error);
        return Promise.resolve();
      });
  };
}
Enter fullscreen mode Exit fullscreen mode
  • Error handling: Always remember to do full error handling in initializer functions. This way, your application can still come up if something breaks during initialization. Log the error for further analysis, but not blocking the user.
export function initializeApp(configService: ConfigService): () => Promise<any> {
  return (): Promise<any> => {
    return configService.loadConfig()
      .catch(error => {
        console.error('Initialization failed:', error);
        return Promise.resolve();  // Prevents app from blocking
      });
  };
}
Enter fullscreen mode Exit fullscreen mode
  • Testing initializer functions: Another important aspect for initializer functions is writing unit tests. It will ensure your initialization logic works correctly with scenarios like network failures, missing configurations, or broken dependencies. This test ensures that an initializer is resolved even in the absence of any configuration.
it('should handle missing config gracefully', async () => {
  const promise = loadConfig(httpClient).call();
  expect(await promise).toBeUndefined();  // Expecting resolution even on failure
});
Enter fullscreen mode Exit fullscreen mode
  • Performance monitoring: Keep track of the performance impact of your initializer functions, more so in production environments. You can use profiling tools available in Angular to find out bottlenecks and work on optimizing them. This could involve task simplification, parallelization, or deferring non-critical tasks until after initialization.

Such best practices will ensure that your usage of APP_INITIALIZER is effective and efficient for a smoother, more reliable application startup process.

Conclusion

You'll need to understand and use APP_INITIALIZER if you want your application to fire up with all configurations and data in place. The powerful tool provides a robust mechanism for running asynchronous initialization tasks and making sure that your app is fully prepared, so it can smoothly present the user with a great experience from the very beginning.

Follow best practices, and avoid falling into common traps so that you can successfully preload configuration settings, initialize third-party services, or even fetch core data. Testing and careful implementation will further instill confidence in your ability to effectively work with APP_INITIALIZER.

Further Reading

Top comments (0)