npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@nx-squeezer/ngx-async-injector

v3.1.0

Published

Angular library that provides async dependency injection

Downloads

51

Readme

@nx-squeezer/ngx-async-injector

CI npm latest version CHANGELOG codecov compodoc renovate semantic-release

Motivation

Angular's dependency injection (DI) system is designed to be synchronous, since having asynchronous providers would make component rendering asynchronous and break existing renderer.

As of today it is not possible to lazy load data asynchronously and consume it through a provider. The only option recommended by Angular when it needs to be loaded before the app initializes is using APP_INITIALIZER. However, it has several known cons because it is blocking and delays rendering the whole component tree and loading routes.

Another common problem is the initial payload of the main bundle caused by needing to declare providers in root. When a provider is needed by various features it usually needs to be declared in the root injector, increasing the initial bundle size. It would be great that services could be declared in the root component, but lazy loaded when needed. It is true that using providedIn: root could be used in many scenarios, but there are others where using async import() of a dependency would be more useful, such as code splitting and fine grained lazy loading.

For the scenarios described above, having a way to declare asynchronous providers, either by loading data from the server and later instantiating a service, or to lazy load them using import(), could help and give flexibility to implementers. This particular problem is what @nx-squeezer/ngx-async-injector solves.

Show me the code

The API that this library offers is very much similar to Angular's DI. Check this code as an example:

// main.ts
bootstrapApplication(AppComponent, {
  providers: [
    {
      provide: MY_SERVICE,
      useClass: MyService,
    },
  ],
});

// component
class Component {
  private readonly myService = inject(MY_SERVICE);
}

Could be made asynchronous and lazy loaded using provideAsync():

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

// component
class Component {
  private readonly myService = inject(MY_SERVICE);
}

That's it! Declaration is almost identical, and consumption is the same. But wait, when is the async provided actually loaded and resolved?

It needs another piece that triggers it: async provider resolvers. Check this diagram:

resolver diagram

Async providers need to be resolved before being used, and that is a responsibility of the application. It can be done while loading a route using a route resolver, or with a structural directive that will delay rendering until they are loaded.

Check this online Stackblitz playground with a live demo.

Examples

Resolve using route's resolver

export const appRoutes: Route[] = [
  {
    path: '',
    loadComponent: () => import('./route.component'),
    resolve: {
      asyncProviders: () => resolveMany(MY_SERVICE),
    },
  },
];

In this case, the async provider will be resolved while the route loads, and the inside the component MY_SERVICE can be injected.

Resolve using a structural directive

@Component({
  imports: [ResolveAsyncProvidersDirective, ChildComponent],
  template: ` <child-component *ngxResolveAsyncProviders="{ myService: MY_SERVICE }" /> `,
  standalone: true,
})
export default class ParentComponent {
  readonly MY_SERVICE = MY_SERVICE;
}

In this case, the async provider will be resolved when the parent component renders, and once completed the child component will be rendered having MY_SERVICE available.

Resolve configuration from API

// Instead of using the common approach of APP_INITIALIZER, which blocks loading and rendering until resolved:
bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(),
    {
      provide: APP_INITIALIZER,
      useFactory: () => inject(HttpClient).get('/config'),
      multi: true,
    },
  ],
});

// You could declare it with an async provider, which will be resolved on demand without blocking,
// and yet available through DI:
bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(),
    {
      provide: CONFIG_TOKEN,
      useAsyncFactory: () => firstValueFrom(inject(HttpClient).get('/config')),
    },
  ],
});

API documentation

Check the full documentation to see all available features.

provideAsync function

It is used to declare one or more async providers. For each provider, it requires the token, and then an async function that can be useAsyncValue, useAsyncClass or useAsyncFactory. It supports multi providers as well. It can be used in environment injectors, modules, components and directives. If multiple providers need to be declared in the same injector, use a single provideAsync function with multiple providers instead of using it multiple times.

Async provider tokens are regular Angular injection tokens typed with the resolved value of the async provider.

Example of declaring a single async provider:

export const MY_SERVICE = new InjectionToken<MyService>('my-service-token');

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

Example of declaring multiple providers, each one with different async functions:

bootstrapApplication(AppComponent, {
  providers: [
    provideAsync(
      {
        provide: CLASS_PROVIDER,
        useAsyncClass: () => import('./first-service').then((x) => x.FirstService),
      },
      {
        provide: VALUE_PROVIDER,
        useAsyncValue: () => import('./value').then((x) => x.value),
      },
      {
        provide: FACTORY_PROVIDER,
        useAsyncFactory: () => import('./factory').then((x) => x.providerFactory),
      }
    ),
  ],
});

// first-service.ts
export class FirstService {}

// value.ts
export const value = 'value';

// factory.ts
export async function providerFactory() {
  return await Promise.resolve('value');
}

Multi providers can also be declared as it happens with Angular:

bootstrapApplication(AppComponent, {
  providers: [
    provideAsync(
      {
        provide: VALUE_PROVIDER,
        useAsyncValue: () => import('./first-value').then((x) => x.value),
        multi: true,
      },
      {
        provide: VALUE_PROVIDER,
        useAsyncValue: () => import('./second-value').then((x) => x.value),
        multi: true,
      }
    ),
  ],
});

Finally, the lazy load behavior can be controlled by the mode flag. By default it is lazy, which means it won't be resolved until requested. eager on the contrary will trigger the load on declaration, even though resolvers are still needed to wait for completion. Example:

bootstrapApplication(AppComponent, {
  providers: [
    provideAsync({
      provide: VALUE_PROVIDER,
      useAsyncValue: () => import('./first-value').then((x) => x.value),
      mode: 'eager',
    }),
  ],
});

When using a factory provider, the function itself can be async. Regular inject function from Angular can be used before executing any async code since the injection context is preserved, however it can't be used afterwards. To solve that problem, and also to protect against cyclic dependencies between async providers, the factory provider function is called with a context that exposes two functions that are self explanatory, inject and resolve. Example:

import { InjectionContext } from '@nx-squeezer/ngx-async-injector';

export async function providerFactory({ inject, resolve }: InjectionContext): Promise<string> {
  const firstString = await resolve(FIRST_INJECTION_TOKEN);
  const secondString = inject(SECOND_INJECTION_TOKEN);
  return `${firstString} ${secondString}`;
}

resolve and resolveMany

resolve and resolveMany functions can be used in route resolvers to ensure that certain async providers are resolved before a route loads. They could be used in other places as needed, since they return a promise that resolves when the async provider is resolved and returns its value. It can be compared to Angular's inject function, but for async providers.

Example of how to use it in a route resolver:

export const routes: Route[] = [
  {
    path: '',
    loadComponent: () => import('./route.component'),
    providers: [
      provideAsync(
        {
          provide: CLASS_PROVIDER,
          useAsyncClass: () => import('./first-service').then((x) => x.FirstService),
        },
        {
          provide: VALUE_PROVIDER,
          useAsyncValue: () => import('./value').then((x) => x.value),
        }
      ),
    ],
    resolve: {
      asyncProviders: () => resolveMany(CLASS_PROVIDER, VALUE_PROVIDER),
    },
  },
];

*ngxResolveAsyncProviders structural directive

This directive can be used to render a template after certain async providers have resolved. It can be useful to delay loading them as much as possible. The template can safely inject those resolved async providers.

When no parameters are passed, it will load all async injectors in the injector hierarchy:

@Component({
  template: `<child-component *ngxResolveAsyncProviders></child-component>`,
  providers: [provideAsync({ provide: STRING_INJECTOR_TOKEN, useAsyncValue: stringAsyncFactory })],
  imports: [ResolveAsyncProvidersDirective, ChildComponent],
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
class ParentComponent {}

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

Additionally, it also supports a map of async provider tokens. Only those will be resolved instead of all. The resolved async providers are available as the context for the structural directive. Example:

@Component({
  template: `
    <!-- Use $implicit context from the structural directive, it is type safe -->
    <child-component
      *ngxResolveAsyncProviders="{ stringValue: stringInjectionToken }; let providers"
      [inputText]="providers.stringValue"
    ></child-component>

    <!-- Use the key from the context, it is type safe as well -->
    <child-component
      *ngxResolveAsyncProviders="{ stringValue: stringInjectionToken }; stringValue as stringValue"
      [inputText]="stringValue"
    ></child-component>
  `,
  providers: [provideAsync({ provide: STRING_INJECTOR_TOKEN, useAsyncValue: stringAsyncFactory })],
  imports: [ResolveAsyncProvidersDirective, ChildComponent],
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
class ParentComponent {
  readonly stringInjectionToken = STRING_INJECTOR_TOKEN;
}

@Component({
  selector: 'child-component',
  template: `Async injector value: {{ inputText }}`,
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
class ChildComponent {
  @Input() inputText!: string;
}

Installation

Do you like this library? Go ahead and use it! It is production ready, with 100% code coverage, protected by integration tests, and uses semantic versioning. To install it:

npm install @nx-squeezer/ngx-async-injector