Skip to main content

Angular Provider Idempotency

When developing libraries, you will come across the need for your library to somehow, convey or pass on, any registration needs it has to the consuming application, or consuming library.

Simple Example

The typical way to do this, is to include a lib-providers file with a function that returns a providers array.

Here's a simple example of a lib-providers.ts:

import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withFetch } from '@angular/common/http';

export const LIB_OGAWEBUISHAREDKERNEL_PROVIDERS: ApplicationConfig =
{
  providers: [provideHttpClient(withFetch())]
};

The above is a simple lib-providers.ts file that a library would contain.
It includes a provider call to register the HttpClient, as the library uses it.

And, the consuming application would simply add that provider call to its providers block, like this:

import { LIB_OGAWEBUISHAREDKERNEL_PROVIDERS } from 'lib-oga-webui-sharedkernel';

export const appConfig: ApplicationConfig = {
  providers:
  [
    provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes),
    LIB_OGAWEBUISHAREDKERNEL_PROVIDERS.providers
  ]
};

The above example is how an app would include a library's needed providers, for DI registration.

Chaining Problem

The previous example is straightforward to reason.
But, it has a flaw.

If we have a library, X, that is consumed by another library, Y. And, both library X and the consuming library X are consumed by an app, then the consuming library Y may include in its provider method, the provider call from library X.

This seems valid.
But if the consuming app, includes both provider methods from X and Y, then the library X provider will be called twice.

So, we need to ensure that providers are idempotent.

Idempotent Solution

When developing a library that has a providers method, we will have it register a simple string marker to tell if the provider has been run, already.

The idempotent lib-providers.ts method would look like this:

import { ApplicationConfig, InjectionToken, injectinject, runInInjectionContext, EnvironmentInjector } from '@angular/core';
import { provideHttpClient, withFetch } from '@angular/common/http';


// Define a unique DI token for our library...
export const LIB_LIBRARYNAME_PROVIDER_REGISTEREDLIBRARYNAME_PROVIDER_REGISTERED = new InjectionToken<boolean>('LIB_LIBRARYNAME_PROVIDER_REGISTERED'LIBRARYNAME_PROVIDER_REGISTERED');


// Declare the exported library provider that will be consumed...
// This is what any consumer will call.
export const LIB_LIBRARYNAME_PROVIDERS:LIBRARYNAME_PROVIDERS: ApplicationConfig = {
  providers: [...provideLibraryProviders()] // CheckInvoke the factory function
};


// Factory function to determine if the providerproviders hasshould already beenbe registered...
// IfTo found,make this work, we use a provider factory, whose lambda does the idempotency check for us, and registers things as required.
function provideLibraryProviders(): any[] {
  return [].
    {
      provide: 'LIBRARYNAME_CONDITIONAL_PROVIDERS',
      useFactory: () => {
        return runInInjectionContext(inject(EnvironmentInjector), () => {

          // IfAttempt notto found, registerretrieve our markerregistration andmark returnfrom the providers.DI...
          //const ThealreadyRegistered ...= at the beginning of this is a spread operator. Be sure to keep it in place.
    ...(inject(LIB_LIBRARYNAME_PROVIDER_REGISTERED,LIBRARYNAME_PROVIDER_REGISTERED, { optional: true });

          ?// Check if we retrieved it...
          if (alreadyRegistered)
          {
            // Already retrieved.

            // Return an empty array...
            return []; :// Providers already registered, return an empty array
          }

          // If here, we've not registered, yet.
          return [

            // Register the Http Client provider...
            provideHttpClient(withFetch()),


            // Include any otheractual providers that you need, here...


            // Now, register our idempotency marker, so we know if we've done this, already...
            // This provider registers a simple string that we use to check if our provider has been called, before.
            // Be sure that this provider stays here.
            { provide: LIB_LIBRARYNAME_PROVIDER_REGISTERED,LIBRARYNAME_PROVIDER_REGISTERED, useValue: true }
          ];
        });
      }
    }
  ];
};

NOTE: Make sure that you create unique string names if using the above example.
Easiest way is to replace each occurrence of 'LIBRARYNAME' with your libraries name.

The above lib-providers.ts file includes some logic that will check if a sentinal string has been registered with DI.


If so, it returns an empty provider.


If not, it returns the actual providers that it needs to register.


This makes our provider idempotent.