Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

asynchronous providers #23279

Open
rehfeldchris opened this issue Apr 10, 2018 · 82 comments
Open

asynchronous providers #23279

rehfeldchris opened this issue Apr 10, 2018 · 82 comments
Assignees
Labels
area: core Issues related to the framework runtime core: di feature: under consideration Feature request for which voting has completed and the request is now under consideration feature Issue that requests a new feature
Milestone

Comments

@rehfeldchris
Copy link

rehfeldchris commented Apr 10, 2018

I'm submitting a...


[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report  
[ X] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

Angular doesn't seem to support providers which asynchronously create their object / service. For those who want to load config from a server dynamically, they seem to be encouraged to use a kludge comprised of creating a ConfigService and then using APP_INITIALIZER to delay the bootstrapping just long enough to make an async call and inject the results into the ConfigService. But, this is a hack, and is buggy with race conditions - the ConfigService may potentially be injected into other services, and be used / read from before this bootstrapping happens, causing bugs because the object gets used before it's ready.

Here's a plunk which demonstrates the problem: https://plnkr.co/edit/plWAT5i5BFtjastSdaVy?p=preview

Notice the problem is that another service will read from the ConfigService before it's truly finished being "constructed" IMO, since we need to construct the object, and then finish initializing it's state the APP_INITIALIZER step. I think reading config in a constructor is pretty common programming practice.

Expected behavior

I think it would be great to be able to do something like:

export function asyncConfigServiceFactory(api: ApiService) {
    return new Promise((resolve, reject) => {
        api.get('my-config.json').then(response => {
            resolve(new ConfigService(response));
        });
    });
}

@NgModule({
  providers: [
    ApiService,
    {
      provide: ConfigService,
      useAsyncFactory: asyncConfigServiceFactory,
      deps: [ApiService]
    },
  ],
})

(notice the useAsyncFactory idea to tell ng that it should wait on the promise, and use the value it produces as the service)
I think that's a pretty straight forward way to do it, but whatever you think is the best way to address it.

What is the motivation / use case for changing the behavior?

My use case is to fetch config from a server, and stick it into a service, and then inject this ConfigService into other services. I do not want to inject ConfigService into other stuff until it's fully constructed and ready to be used - in other words, until the asynchronous request to the server has completed and my ConfigService has all the values populated into it.

I've seen quite a few other users trying to accomplish the same thing on Stackoverflow. This kludge is becoming the defacto way to do it, and SO answers are getting highly upvoted, and multiple articles are being published on other websites - all recommending a bug-prone method. That's not good for the framework's reputation long term. It should be easy to do it "the right way", whatever that is.

Environment


Angular version: 5.1



Others:

Other issues that would be well served by async providers:
#19448
#20383

@IgorMinar IgorMinar added the area: core Issues related to the framework runtime label Apr 10, 2018
@ngbot ngbot bot added this to the needsTriage milestone Apr 10, 2018
@vicb vicb added the feature Issue that requests a new feature label Apr 10, 2018
@ngbot ngbot bot modified the milestones: needsTriage, Backlog Apr 10, 2018
@mischkl
Copy link

mischkl commented Apr 16, 2018

Having exactly this problem currently! ConfigService initialized with APP_INITIALIZER, but it's too late because some services are bootstrapped beforehand. :(

@mlc-mlapis
Copy link
Contributor

@mischkl ... but isn't it just a different organizational point of view ... because:

  • if you put the code to just one ConfigService, it would work
  • or because APP_INITIALIZER is a multi provider ... you can have more services ... and just their declaration order in providers section of the main app module decides which is initialized first.

@mischkl
Copy link

mischkl commented Apr 16, 2018

The problem is that some services are defined in other modules and imported into the app module, in which case they are initialized first.

@mlc-mlapis
Copy link
Contributor

@mischkl ... hmmm, I don't think so ... APP_INITIALIZER tokens as multi providers and their dependencies are initialized first. All the rest is waiting. The problem above in the Plunker was deps: [ConfigService, ApiService] ... and the fact that ApiService is also the dependency for ConfigService.

@trotyl
Copy link
Contributor

trotyl commented Apr 17, 2018

FYI, there're indeed asynchronous DI support in the very alpha: #1623 #2813

But have already been dropped for years.

@mischkl
Copy link

mischkl commented Apr 17, 2018

@mlc-mlapis that isn't the problem with my project AFAIK. In my case I am using Ngrx, and the Effects in a (non-lazy-loaded) feature module depend on services which in turn try to retrieve the backend URL from the ConfigService in their constructors. On the other hand, if I were to inject the service into a component ("classic" Angular as opposed to redux) it wouldn't be a problem because components are initialized after APP_INITIALIZER.

In my experience it is particularly a problem when using Ngrx Effects. As the plunkr demonstrates there may be other ways to reproduce the problem, but at least when using Effects I haven't found a way to solve the problem via structural changes.

There are workarounds, of course. One is to dynamically grab the values on every service method call. Another is to expose a promise on the ConfigService and let the API services use a .then() in the constructor. The main complaint for me is simply that APP_INITIALIZER is only a partial solution since it only holds up component initialization but doesn't know anything about the service side of things. The obvious solution would be to introduce async provider initialization.

@joeldavuk
Copy link

There are structural workarounds but this does seem like a basic feature I would agree with @rehfeldchris that there isn't a "right way" to do this at present and this causes a lot of a confusion.

@zlepper
Copy link

zlepper commented May 29, 2018

Another way to work around this, at least until angular actually support async providers is to inject a value object, which then gets updated using Object.assign(valueObj, serverResponse).

It's still hacky, but should at least allow other client services to use a more simple object. Just beware, this doesn't actually solve the problem of ensuring the configs are ready. It just makes the semantics of getting them a bit more elegant.

@rehfeldchris
Copy link
Author

rehfeldchris commented Jun 3, 2018

For those that don't mind slowing down the bootstrapping process a tiny bit extra, you can do the async requests before bootstrapping. It's messy, but it's a reliable option until we get something better:

// main.ts
function bootstrapFailed(val) {
    document.getElementById('bootstrap-fail').style.display = 'block';
    console.error('bootstrap-fail', val);
}

fetch('config.json')
    .then(response => response.json())
    .then(config => {
        if (!config || !config['isConfig']) {
            bootstrapFailed(config);
            return;
        }

        // Store the response somewhere that your ConfigService can read it.
        window['tempConfigStorage'] = config;

        platformBrowserDynamic()
            .bootstrapModule(AppModule)
            .catch(bootstrapFailed);
    })
    .catch(bootstrapFailed);

and then use a more normal service creation strategy

// app.module.ts
export function configServiceFactory() {
    return new ConfigService(window['tempConfigStorage']);
}

@NgModule({
    providers: [{provide: ConfigService, useFactory: configServiceFactory}],
    bootstrap: [AppComponent]
})
export class AppModule {}

I'd still rather see framework level support for this. It will make it easy to use, for example, an ApiService to load the config, and it will probably perform better if the framework does it. The above method likely forfeits some browser resource loading / work parallelism by making everything wait for the config so early in the bootstrap processing, before other work might get started.

@jimbarrett33
Copy link

A solution to this would also promote real re-usable Angular libraries. We have an Angular 6 Library that is just domain services that I want others in the org to use. Currently, configured like MyModule.forRoot( environment.whateverConfig ) in app.module.ts. The issue is some of the settings in environment.whateverConfig need to be provided by a call to the server for which we use APP_INITIALIZER. The problem is forRoot() is executed before the promise in the APP_INITIALIZER provider finishes, so the library does not get the correct config.

If someone on the Angular team could comment on the priority of this item and if/when it is on their radar to implement, that would be great.

@gagle
Copy link

gagle commented Aug 19, 2018

This feature would be so useful for implementing reusable services in a more natural way only by injecting object-value tokens.

I also have one npm package with domain services. These services receive the base url for all the endpoints with a token. This url comes from a fetch to the server. Currently the only way to achieve this is by fetching the url before the angular boot and then creating an extra provider that I pass to the bootstrap function instead of using a global variable.

The workaround is to have some setup(config) function in the service that is called from an app_initializer to make use of async factory but I prefer the first option, it's more cleaner. The problem is that you're forcing the consumers to fetch the configuration before Angular.

Any news on this?

@gogakoreli
Copy link

gogakoreli commented Aug 20, 2018

I have the same problem. I am trying to provide ConfigService using APP_INITIALIZER and use ConfigService to provide InjectionToken via useFactory, which has dependency on ConfigService. The problem is that inside the useFactory I get the ConfigService but it is not resolved yet. ConfigService should have config object ready to use, which is not true. By the way console.log inside useFactory is executed first and then comes the console.log from inside the APP_INITIALIZER.

I want to have finished ConfigService with ready to use config file and then provide InjectionToken via using this ConfigService

@mdarefull
Copy link

+1 I wanted to add my to support this feature request. Angular has grown a lot since its first release and the entire community has grown as well, patterns has been creating and a lot of idea has been adopted from other successful frameworks as well.
Currently, it is rare to find a project that hasn't adopted Reactive Programming or even better, Redux using ngrx. Most of the modern approaches are around Observables and hence it makes sense to also update the way we create our providers.

Specifically to my case, we are using a 3rd party library with services that expect to receive configuration data as Injection Tokens. We are storing all of our data, particularly configuration on our store and the way to access it is through selectors which returns observable.
I can only imagine that with time they will appear many more use cases because observables are everywhere so, to me, it makes perfect sense to make angular provide built-in support for these scenarios.

@MatthewDavidCampbell
Copy link

Dealing with JIT is doable even for forRoot statics. Just add dynamic imports to do the AppModule import after grabbing stuff from the server [main.js]:

fetch('some url')
  .then(x => save config stuff to window | storage | etc)
  .then(_ => import('./path/to/app.module'))
  .then(({AppModule}) => {
    platformBrowserDynamic().bootstrapModule(AppModule);
  });

Great for JIT. Not so great with AOT. Would love ideas on AOT. Maybe a dynamic export that the AOT plugin can handle?

@WhitWaldo
Copy link

Just wanted to voice my +1 for this as well. I'm not presently using Angular, but am using its injection functionality via inject-js. Ideally I'd like to be able to return an observable from my factory provider to register that I can subsequently inject.

@anymos
Copy link

anymos commented Jan 2, 2019

Same here +1, this is deeply needed especially when you have a complex app growing ...

@anymos
Copy link

anymos commented Jan 2, 2019

Use case :

A lib provides authentification, which is configured by a provider as :
UserModule.forRoot({fb: {
appId: 'fbkeyid-dynamicvalue'
}});

in a multi-tenant application, the fbkeyid-dynamicvalue should be retrieved server side, asynchronously.

Happy to have a solution on this, unless that I miss it, there is none ...

Of course, considering without modifying the UserModule lib ...

Pretty standard use case. By the way, was it not possible in angular 1.X ?

@jcimoch
Copy link

jcimoch commented Feb 1, 2019

Is it going to be developed after ivy is ready?

@Tahiche
Copy link

Tahiche commented Feb 13, 2019

This is extremely frustating. I beleive my case is similar to @anymos
I´m using angular-oauth2-oidc and it expects a moduleConfig object in forRoot(), which it then exposes as a provider.
After too many hours i´m frustratingly giving up.

This is the relevant part in my forRoot() providers[]

{ provide: OAuthModuleConfig,
         useFactory: (service: ConfigService) => {
             service.loadConfig().then(
              (res) => {
                console.log('service.loadConfig res', res);
                let loadedConfig = res as OAuthModuleConfig;
               // loadedConfig is the expected Object, everything looks right
              // but what´s returned below is a ZoneAwarePromise and the loadedconfig object is in 
             // __zone_symbol__value, so basically useless
                return loadedConfig;
              }
            );
        },
          deps: [ConfigService]
        },

so "loadedConfig" is returned as a ZoneAwarePromise of sorts, there´s no way to assign the actual value. It´s there, but you can´t assign it. It´s async but I can´t use the result...

I see no other option but to use a window object (script src="appsettings.js") , because that WILL be available and it´s the only way i can think of keeping the config separate without too much hassle.

I hope they provide a solution for this.

@chiranjeevi-puvvula-ascendlearning-com
Copy link

+1

@jimbarrett33 , I'm having the same issue . Did you find a temporary solution to the problem. Before I go with the workaround of fetching data during bootstap I want to check with you.

@jimbarrett33
Copy link

jimbarrett33 commented Apr 10, 2019

@chiranjeevi-puvvula-ascendlearning-com

Still no other solution other than using fetch. The fetch solution is not that bad unless it's a huge configuration and even then...

  1. it's likely that much of the stuff you're trying to configure can be baked Angular environment files so it's not needed to configure dynamically
  2. generally, not everything has to be configured to support the initial loading of the app. So, for example, just use dynamic config for what's needed to show the user the first first view and put the rest on a timer or something.

I ended up fetching only dynamic config required for services (mainly endpoint config stuff) and for those services, which had already been loaded in forRoot(), called an "updateConfig()" method on them in the "initialize" service after the fetch.

@dancju
Copy link

dancju commented Apr 21, 2019

This feature is necessary.

NestJS (which is a node.js backend framework yet similar to Angular) supports asynchronous providers[1], and it is a highly praised feature.

@kamilmysliwiec Can you help implementing this async-provider-loader for Angular? 😂

[1] https://docs.nestjs.com/fundamentals/async-providers

@maplion
Copy link

maplion commented Sep 14, 2021

I'm still struggling with this. I just want to be able to dynamically load an appConfig asynchronously and I can't quite get things to work. Some version of the above solutions get me most of the way there, but my APP_CONFIG token injections get called before it is loaded because the modules load things that need it prior to the process completing -- or some other form of race condition. I've tried many different amalgamations and permutations to try to resolve the races consistently, but nothing works consistently as fetching before the application loads in the main.ts -- something that no longer is supported by PWA applications, so I need another solution.

The following is what has worked for me for the past 2 years:

main.ts

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { APP_CONFIG } from '@core/constants/app.config';
import { EnvService } from '@core/services/env.service';
import { AppModule } from './app/app.module';
import { IAppConfig } from './app/core/interfaces/app-config.interface';
import { envServiceFactory } from './app/core/services/env.service.provider';


const envService = envServiceFactory();  // This gets the environment from a javascript file

if (envService.production) {
    enableProdMode();
}

let appConfig: IAppConfig;

fetch(`./assets/configs/app/app-config.${envService.environmentName}.json`)
    .then(async res => {
        appConfig = await res.json();
        appConfig.version = envService.version;
        appConfig.updatedDate = envService.updatedDate;
        appConfig.environmentName = envService.environmentName;
        appConfig.production = envService.production;
    }).catch(err => console.error(`Fatal Error: app-config.${envService.environmentName}.json failed to load: ${err}`))
    .then(next => {
        platformBrowserDynamic([
            { provide: EnvService, useValue: envService },
            { provide: APP_CONFIG, useValue: appConfig },
        ]).bootstrapModule(AppModule).then(ref => {
            // Ensure Angular destroys itself on hot reloads.
            if (window['ngRef']) {
                window['ngRef'].destroy();
            }
            window['ngRef'] = ref;

            // Otherwise, log the boot error
        }).catch(err => console.error(err));
    }).catch(err => console.error(`Fatal Error: Configuration Failed to Load. ${err}`));

I have never successfully accomplished the same thing as above loading it within app.module. The closest I've come is:

app.module.ts

        {
            provide: APP_CONFIG,
            deps: [EnvService, HttpClient],
            useFactory: async (envService: EnvService, http: HttpClient, store: Store): Promise<IAppConfig> => {
                return await appConfigServiceFactory(envService, http)
                    .then((appConfig: IAppConfig) => {
                        localStorage.setItem('appConfig', JSON.stringify(appConfig));
                        return appConfig;
                    });
            }
        },

app-config.service.ts

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { setAppConfig } from '@core/actions/core.actions';
import { IAppConfig } from '@core/interfaces/app-config.interface';
import { Store } from '@ngrx/store';
import { firstValueFrom } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { EnvService } from './env.service';

@Injectable({
    providedIn: 'root'
})
export class AppConfigService {
    context = AppConfigService.name;

    constructor() { }

    async loadConfig(envService: EnvService, http: HttpClient): Promise<IAppConfig> {
        console.log(this.context, '> ',
            `Loading app-config from environment: ${envService.environmentName}`);
        return await firstValueFrom(
            http.get(`./assets/configs/app/app-config.${envService.environmentName}.json`)
                .pipe(
                    map((appConfig: IAppConfig) => {
                        appConfig.version = envService.version;
                        appConfig.updatedDate = envService.updatedDate;
                        appConfig.environmentName = envService.environmentName;
                        appConfig.production = envService.production;
                        return appConfig;
                    }),
                    catchError(error => {
                        console.log(`Fatal Error: App Config failed to load: ${error}`);
                        throw new Error(error);
                    })
                )
        );
    }
}

export const appConfigServiceFactory = async (envService: EnvService, http: HttpClient, store: Store): Promise<IAppConfig> => {
    const appConfigService = new AppConfigService();
    return await appConfigService.loadConfig(envService, http);
};

But APP_CONFIG is not populated early enough and services loaded from my CoreModule or components that need DI injection of the token need it before it is ready and no matter what I try, I can't seem to get an architecture that makes them wait long enough.

This has to be a pretty common thing and it shouldn't be this hard. Any suggestions would be appreciated.

@gmaughan
Copy link

something that no longer is supported by PWA applications, so I need another solution.

@maplion I've been using a solution I mentioned above in PWA applications successfully for a while now.

See this StackOverflow answer for details.

It uses XMLHttpRequest rather than fetch to build the configuration before bootstrapping the Angular module.

I agree, it shouldn't be this hard.

Glenn.

@maplion
Copy link

maplion commented Sep 14, 2021

@gmaughan This looks promising. I'll try it out, thank you!

@maplion
Copy link

maplion commented Oct 2, 2021

@gmaughan You're the man; your solution works exactly like I need.

@Hafnernuss
Copy link

Unfortunately, none of the above mechanisms worked for me. I need to initialize a module with configuration, and unfortunately even with @gmaughan's approach, those modules were imported before the code ran.

This approach however worked flawlessly.

@coreConvention
Copy link

I would LOVE to see this feature! Would open up so many possibilities. Why is this still not a thing?

@Yeyu83
Copy link

Yeyu83 commented Jun 6, 2022

Hi guys! Check this repo maybe could bring you an optimal solution...

https://github.com/Yeyu83/async-data-before-bootstrap

@alxhub
Copy link
Member

alxhub commented Jun 24, 2022

Angular's DI system is designed to be synchronous, and we have no plans to make this kind of fundamental change, for a few reasons:

  1. This would force the entire runtime to become painfully asynchronous.

Rendering in Angular today is synchronous. When a component <some-cmp> is referenced in a template, that component is instantiated and rendered along with the rest of the template.

Having asynchronous services would make component rendering asynchronous, because any individual component could not be created without perhaps waiting for a dependency to resolve. The same is true for all DI APIs, such as Injector.get() - they would have to return a Promise because the resulting instantiation might be async. Also, all rendering APIs - ViewContainerRef.createComponent for example. Having every component/directive rendering operation go through a Promise would likely have a hugely negative impact on rendering performance.

  1. DI is the wrong layer to address async dependencies.

If we make rendering asynchronous, not only is it bad for performance, but it limits how the application can surface that it's currently in a loading state. Components may want to render a loading indicator, play animations, and give other signals to the user that they're currently blocked. If the framework is forced to block before it can even instantiate the component, there's no opportunity to customize the behavior - the user just stares at a blank screen or (worse) a half rendered component.

Instead, asynchronous dependencies should be modeled within the values injected, not within the DI system itself. That is, you can provide APP_CONFIG with an InjectionToken<Promise<...>> and await the promise where the config is needed, or use APP_INITIALIZER to block the whole bootstrap process until the config is available. This can be done fairly cleanly:

export function provideSafeAsync<T>(
  token: T | InjectionToken<T>,
  initializer: () => Promise<T>
): Provider[] {
  const container: { value?: T } = { value: undefined };
  return [
    {
      provide: APP_INITIALIZER,
      useValue: async () => {
        container.value = await initializer();
      },
      multi: true,
    },
    {
      provide: token,
      useFactory: () => {
        if (!inject(ApplicationInitStatus).done) {
          throw new Error(
            `Cannot inject ${token} until bootstrap is complete.`
          );
        }
        return container.value;
      },
    },
  ];
}

@mlc-mlapis
Copy link
Contributor

mlc-mlapis commented Jun 25, 2022

@alxhub Thank you for your arguments and logical justifications. There are several fundamental ideas and conceptual links in them. But isn't the initial problem most developers come up with when talking about asynchronous providers that they are looking for a way to initialize some setup parameters before module imports are processed?

@DaSchTour
Copy link

From the discussion maybe it would be a good idea to have a documentation on this topic. Or maybe some helper function like shown above with documentation and usage examples. As I didn't manage to do this I've build a wrapper around the bootstrap process and use side effects to store data for initializing the application. But I would like to have some process that is more angular like.

@pkozlowski-opensource
Copy link
Member

Completely agree with Alex's assessment. The good news that we've got more hooks now into providers definition, most notably on the route level. See the related discussion in #17606 (comment)

@SETI-At-Home
Copy link

We definitely need some kind of "silver bullet approach" for this kind of issues. Often we need appsettings in Angular.

@PhilippRoessner
Copy link

Is there any chance to see a usage example of @alxhub 's Answer? How can I make a HttpClient Call with this approach during startup?

@bryanrideshark
Copy link

Angular's DI system is designed to be synchronous, and we have no plans to make this kind of fundamental change, for a few reasons:

  1. This would force the entire runtime to become painfully asynchronous.

Rendering in Angular today is synchronous. When a component <some-cmp> is referenced in a template, that component is instantiated and rendered along with the rest of the template.

Having asynchronous services would make component rendering asynchronous, because any individual component could not be created without perhaps waiting for a dependency to resolve. The same is true for all DI APIs, such as Injector.get() - they would have to return a Promise because the resulting instantiation might be async. Also, all rendering APIs - ViewContainerRef.createComponent for example. Having every component/directive rendering operation go through a Promise would likely have a hugely negative impact on rendering performance.

  1. DI is the wrong layer to address async dependencies.

If we make rendering asynchronous, not only is it bad for performance, but it limits how the application can surface that it's currently in a loading state. Components may want to render a loading indicator, play animations, and give other signals to the user that they're currently blocked. If the framework is forced to block before it can even instantiate the component, there's no opportunity to customize the behavior - the user just stares at a blank screen or (worse) a half rendered component.

Instead, asynchronous dependencies should be modeled within the values injected, not within the DI system itself. That is, you can provide APP_CONFIG with an InjectionToken<Promise<...>> and await the promise where the config is needed, or use APP_INITIALIZER to block the whole bootstrap process until the config is available. This can be done fairly cleanly:

export function provideSafeAsync<T>(
  token: T | InjectionToken<T>,
  initializer: () => Promise<T>
): Provider[] {
  const container: { value?: T } = { value: undefined };
  return [
    {
      provide: APP_INITIALIZER,
      useValue: async () => {
        container.value = await initializer();
      },
      multi: true,
    },
    {
      provide: token,
      useFactory: () => {
        if (!inject(ApplicationInitStatus).done) {
          throw new Error(
            `Cannot inject ${token} until bootstrap is complete.`
          );
        }
        return container.value;
      },
    },
  ];
}

What if there was a new category of provider, which was only made available to templates?

Imagine the following hypothetical:

@Component({ 
   standalone: true,
   template: `
   <div *ngIf="userIsLoggedIn">
      Only show if the user is logged in.
   </div>
   <div *ngIf="userIsAdministrator">
      Show expensive stuff that only applies if the user is an admin; Load expensive admin-only components and modules to boot.
   </div>
   `,
   templateProviders: [{
     provide: 'userIsLoggedIn',
     async: {
       defaultValue: false,
       useFactory: () => import('get-user-is-logged-in.function').then(m => m.getUserIsLoggedInFunction),
       deps: () => [import('user-service').then(m => m.UserService)]
     }
   },
   {
     provide: 'userIsAdministrator',
     async: {
       defaultValue: false,
       useFactory: () => import('get-user-is-administrator.function').then(m => m.getUserIsAdministratorFunction),
       deps: () => [
          import('expensive-and-inefficient-admin-user-service').then(m => m.AdminUserService),
          import('another-expensive-dependency-to-demonstrate').then(m => m.AnotherExpensiveDependency)
          ]
     }
   }
   ]
})
export class Foo {}

@ciekawy
Copy link

ciekawy commented Oct 24, 2022

2. DI is the wrong layer to address async dependencies.

there is definitely number of cases where various workaround ar in use instead of providing reasonable solution within Angular. One more example is when you try to combine ErrorHandler provider (where an instance need to provided synchronously and early) with some external tracking tool and any async configuration - eg I have similar problem to this bugsnag initialisation - I'd like to share tracked bugsnag session in browser extension and I need async to read the session.

I did hope that I could eg use deps: [APP_INITIALIZER] and expect this to be a case where I can expect the proposed provideSafeAsync to be already in a resolved state. This way we could avoid introducing general asynchronous DI but also provide reasonable way to just make the async APP_INITIALIZER helpful for quite a lot of cases.

@Lonli-Lokli
Copy link

But you actually can use app_initializer for that

@ciekawy
Copy link

ciekawy commented Oct 24, 2022

@Lonli-Lokli so the case is that

  1. ErrorHandler provider needs to return an instance even before the main here in the application-ref
  2. BugsnagErrorHandler needs the Bugsnag client to be already initialised
  3. to create Bugsnag client I need to read the sessionID from browser.storage.local which has async access.
    Of course this is quite a corner case and you can always expect the vendor should consider that. For now I use a workaround and I'd prefer to relay on Angular support

@Lonli-Lokli
Copy link

Lonli-Lokli commented Oct 24, 2022

@ciekawy Yep, but on errorHandler init line all app_initializer deps should be already created, ie even async loaded.
Unfortunately it's not guaranteed with angular and the only way to make it work is to put canactivate guard with required dependency on first route to make it load.
If it's not clear, you can create a sample with not working behaviour and I will try to adopt to make it work

@ciekawy
Copy link

ciekawy commented Oct 24, 2022

route is way too late. I can always do typical approach to use

(async () =>
  platformBrowserDynamic([{
    provide: SESSION_ID,
    useValue: await resolveSessionId()
  }]).bootstrapModule(BackgroundModule);
)();

I believe it should be possible to handle async providers (provide resolved values) within APP_INITIALIZER as anyway returning promise/observable will block app initialisation.

@Lonli-Lokli
Copy link

Route is never late as it creating now before app component

@ciekawy
Copy link

ciekawy commented Oct 24, 2022

In my case I need to resolve async value to create ErrorHandler (and anyway I want errors to be intercepted from the very beginning including initialisation phase) and secondly I'm reusing some logic in browser extension background script having no routes (I know the second is pretty specific)

@ak99372
Copy link

ak99372 commented Dec 24, 2022

While APP_INITIALIZER works well for app initial loading, it does not work for lazy loaded modules that also need to initialize their providers long after the app started. Scenarios where provider configuration might not be known at app start or where providers are incrementally setup/loaded later in the app's run. We're on 15th version while still thinking in terms of rigid and statically configured monoliths without async option (not even suggested workaround/example after almost 5 years)?

@samuelfernandez
Copy link

This discussion is very much interesting. Several reasons have been mentioned on why Angular itself can't implement async providers by design. However, from an application perspective having the option of declaring an async provider that can be lazily resolved is something that might be needed.

That is why I've created a library to solve this problem with an ergonomic API 🎉

@nx-squeezer/ngx-async-injector

npm latest version

There you can find all the documentation, but as an appetizer check this example of the possibilities it offers:

// main.ts
bootstrapApplication(ParentComponent, {
  providers: [
    provideAsync({
      provide: MY_SERVICE,
      useAsyncClass: () => import('./my-service').then((x) => x.MyService),
    }),
  ],
});

// parent.component.ts
@Component({
  template: `<child-component *ngxResolveAsyncProviders></child-component>`,
  imports: [ResolveAsyncProvidersDirective, ChildComponent],
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
class ParentComponent {}

// child.component.ts
@Component({
  selector: 'child-component',
  template: `Async injector value: {{ service.value }}`,
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
class ChildComponent {
  readonly service = inject(MY_SERVICE);
}

Async providers are declared through provideAsync with a very similar API to the one that Angular has. Resolving the provider can be a lazy process, the application has control over it and can trigger that process in a route resolver or through a structural directive. Consumers can inject that resolved provider without even knowing it was async.

The library is fully featured, internally using an async injector. Providers can be declared with useAsyncValue, useAsyncClass or useAsyncFactory. They can also be multi, and provides control over eager or lazy loading. It should provide the glue between an async operation and Angular's DI.

Check this online Stackblitz playground with a live demo.

Please give it a try, feedback is welcome! 😄

@smaillns
Copy link

smaillns commented Nov 7, 2023

I'm facing the same issue, there are some services initialized before providing APP_INITIALIZER, and I'm also experiencing the issue when using a library mentioned by @jimbarrett33 link . is there any relevant solution ? thanks in advance

@Azbesciak
Copy link

As a 300th "liking" of this issue I would ask is it going to happen?

@WhitWaldo
Copy link

As a 300th "liking" of this issue I would ask is it going to happen?

It's been 5 years with no traction - don't hold your breath.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: core Issues related to the framework runtime core: di feature: under consideration Feature request for which voting has completed and the request is now under consideration feature Issue that requests a new feature
Projects
Feature Requests
Close with Followup
Development

No branches or pull requests