angulardependency-injectionangular-http-interceptorsangular-ssrangular18

Angular 18: set InjectionToken after APP_INITIALIZER provider


In angular 18, I need to set API URL after the app initialisation(because for obvious reason, fetching of URL from a json file). However it is not setting. In fact InjectionToken has set before APP_INITIALIZER even though it was added later in providers array in app.config.ts

Reference: https://angular.dev/api/core/APP_INITIALIZER?tab=usage-notes

Here is theStackblitz

expecting to set API_URL_TOKEN to url value and use it further for all the API calls. store in localStorage and assign.

private apiUrl = inject(API_URL_TOKEN);

loginUser(data: { username: string; password: string }) {
    return this.http.post(`${this.apiUrl}/auth`, data).pipe(
      tap((res) => {
        if (res.success) {
          this.addToken(res.data);
        }
      })
    );
  }

Here is what I tried

main.ts

bootstrapApplication(AppComponent, appConfig)

app.config.ts

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withFetch(), withInterceptors([authInterceptor, errorInterceptor])),
    provideAppInitializer(),
    provideApiUrl(),
    provideRouter(routes),
    provideAnimationsAsync(),
    provideCharts(withDefaultRegisterables()),
  ],
};

app-initializer.ts

const CONFIG_URL = '/config.json';

function appInitializer(http: HttpClient, storageService: StorageService) {
  return async () => {
    try {
      const config = await firstValueFrom(http.get<{apiUrl: string}>(CONFIG_URL));
      storageService.apiUrl = config.apiUrl;
      console.log('Config loaded successfully');
    } catch (error) {
      console.error('Error loading configuration:', error);
      throw error;
    }
  };
}

export function provideAppInitializer(): Provider {
  return {
    provide: APP_INITIALIZER,
    useFactory: appInitializer,
    deps: [HttpClient, StorageService],
    multi: true,
  };
}

app-url-token.ts

export const API_URL_TOKEN = new InjectionToken<string>('API_URL_TOKEN');

function apiUrlFactory(storageService: StorageService): string {
  const apiUrl = storageService.apiUrl;
  if (apiUrl) {
    return apiUrl;
  }
  throw new Error('API URL not found in configuration');
}

export function provideApiUrl(): Provider {
  return {
    provide: API_URL_TOKEN,
    useFactory: apiUrlFactory,
    deps: [StorageService],
    multi: false,
  };
}

/* another way */

export const API_URL_TOKEN = new InjectionToken<string>('API_URL_TOKEN');

export function provideApiUrl(): Provider {
  return {
    provide: API_URL_TOKEN,
    useValue: (storageService: StorageService) => storageService.getApiUrl,
    deps: [StorageService],
  };
}

storage.service.ts

import { inject, Injectable, InjectionToken } from '@angular/core';

const LOCAL_STORAGE = new InjectionToken<Storage>('Browser Storage', {
  providedIn: 'root',
  factory: () => localStorage,
});

const API_URL = 'apiUrl';

@Injectable({
  providedIn: 'root',
})
export class StorageService {
  private readonly storage = inject<Storage>(LOCAL_STORAGE);

  get(key: string) {
    return this.storage.getItem(key);
  }
  set(key: string, value: string): void {
    this.storage.setItem(key, value);
  }

  remove(key: string): void {
    return this.storage.removeItem(key);
  }

  clear() {
    return this.storage.clear();
  }

  get apiUrl(): string | null {
    return this.storage.getItem(API_URL) || null;
  }

  set apiUrl(url: string) {
    this.storage.setItem(API_URL, url);
  }
}

On start up

enter image description here

on refresh

enter image description here

In second case its clear that, it is before the initialiser

About config.json

enter image description here


Solution

  • The below github issue helped me understand what went wrong.

    HTTP_INTERCEPTOR does not wait for APP_INITIALLIZER

    Since your interceptor uses HttpClient, this in turn calls the interceptor hence you are getting this issue. The solution to this problem is to use HttpBackend whose requests are not intercepted by the interceptor, this will solve your problem. The input should be passed as HttpRequest

    function appInitializer(http: HttpBackend, storageService: StorageService) {
      return () => {
        return http.handle(new HttpRequest('GET', CONFIG_URL)).pipe(
          map((config: any) => {
            storageService.apiUrl = config.apiUrl;
            console.log('Config loaded successfully');
          }),
          catchError((error: any) => {
            console.error('Error loading configuration:', error);
            throw error;
          })
        );
      };
    }
    
    export function provideAppInitializer(): Provider {
      return {
        provide: APP_INITIALIZER,
        useFactory: appInitializer,
        deps: [HttpBackend, StorageService],
        multi: true,
      };
    }
    

    Place your config.json in an assets folder,to fetch the data. Then configure this in the angular.json's assets folder, so that it will be discoverable. As seen in the stackblitz.

       ...
       },
          "options": {
            "assets": ["src/assets"],
            "index": "src/index.html",
            "browser": "src/main.ts",
            ...
    

    You can get rid of the async await and do this with pure rxjs. we can use map to assign the values to storage and use catchError to catch any exceptions.

    function appInitializer(http: HttpClient, storageService: StorageService) {
      return () => {
        return http.get<XiConfig>(CONFIG_URL).pipe(
          map((config: any) => {
            storageService.apiUrl = config.apiUrl;
            console.log('Config loaded successfully');
          }),
          catchError((error: any) => {
            console.error('Error loading configuration:', error);
            throw error;
          })
        );
      };
    }
    

    One more Thing is you need to import the app config to bootstrap application.

    bootstrapApplication(App, appConfig);
    

    Then finally access this on the component.

    @Component({
      selector: 'app-root',
      standalone: true,
      template: `
        <h1>Hello from {{ name }}!</h1>
        <a target="_blank" href="https://angular.dev/overview">
          Learn more about Angular
        </a>
      `,
    })
    export class App {
      name = 'Angular';
    
      constructor(@Inject(API_URL_TOKEN) private urlToken: string) {
        console.log(urlToken);
      }
    }
    

    Full Code:

    Main.ts

    import { Component, Inject } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import 'zone.js';
    import { appConfig } from './app/app.config';
    import { API_URL_TOKEN } from './app/app-url-token';
    
        @Component({
          selector: 'app-root',
          standalone: true,
          template: `
            <h1>Hello from {{ name }}!</h1>
            <a target="_blank" href="https://angular.dev/overview">
              Learn more about Angular
            </a>
          `,
        })
        export class App {
          name = 'Angular';
    
          constructor(@Inject(API_URL_TOKEN) private urlToken: string) {
            console.log(urlToken);
          }
        }
    
    bootstrapApplication(App, appConfig);
    

    app.config.ts

    import { ApplicationConfig } from '@angular/core';
    import {
      provideHttpClient,
      withFetch,
      withInterceptors,
    } from '@angular/common/http';
    import { provideAppInitializer } from './app-initializer';
    import { provideApiUrl } from './app-url-token';
    //import { provideRouter } from '@angular/router';
    //import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
    //import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
    //import { routes } from './app.routes';
    //import { authInterceptor } from './interceptors/auth.interceptor';
    //import { errorInterceptor } from './interceptors/error.interceptor';
    
    export const appConfig: ApplicationConfig = {
      providers: [
        provideHttpClient(withFetch(), withInterceptors([])),
        provideAppInitializer(),
        provideApiUrl(),
        //provideRouter(routes),
        //provideAnimationsAsync(),
        //provideCharts(withDefaultRegisterables()),
      ],
    };
    

    app.url.token

    import { InjectionToken, Provider } from '@angular/core';
    import { StorageService } from './storage.service';
    
    export const API_URL_TOKEN = new InjectionToken<string>('API_URL_TOKEN');
    
    function apiUrlFactory(storageService: StorageService): any {
      console.log(storageService.apiUrl);
      const apiUrl = storageService.apiUrl;
      if (apiUrl) {
        console.log('set apiUrl');
        return apiUrl;
      }
      throw new Error('API URL not found in configuration');
    }
    
    /*
    
    export const API_URL_TOKEN = new InjectionToken<string>('API_URL_TOKEN');
    
    export function provideApiUrl(): Provider {
      return {
        provide: API_URL_TOKEN,
        useValue: (storageService: StorageService) => storageService.getApiUrl,
        deps: [StorageService],
      };
    }
    
    
    */
    
    export function provideApiUrl(): Provider {
      return {
        provide: API_URL_TOKEN,
        useFactory: apiUrlFactory,
        deps: [StorageService],
        multi: false,
      };
    }
    

    App initializer:

    import { HttpBackend, HttpClient, HttpRequest } from '@angular/common/http';
    import { APP_INITIALIZER, Provider } from '@angular/core';
    import { firstValueFrom } from 'rxjs';
    import { StorageService } from './storage.service';
    import { map, catchError } from 'rxjs';
    
    interface XiConfig {
      apiUrl: string;
    }
    
    const CONFIG_URL = '/assets/config.json';
    
        function appInitializer(http: HttpBackend, storageService: StorageService) {
          return () => {
            return http.handle(new HttpRequest('GET', CONFIG_URL)).pipe(
              map((config: any) => {
                storageService.apiUrl = config.apiUrl;
                console.log('Config loaded successfully');
              }),
              catchError((error: any) => {
                console.error('Error loading configuration:', error);
                throw error;
              })
            );
          };
        }
    
        export function provideAppInitializer(): Provider {
          return {
            provide: APP_INITIALIZER,
            useFactory: appInitializer,
            deps: [HttpBackend, StorageService],
            multi: true,
          };
        }
    

    Stackblitz Demo