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
NOTE: Problem
The previoususage 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 andof the consumingspread libraryoperator 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, inject, runInInjectionContext, EnvironmentInjector } from '@angular/core';
import { provideHttpClient, withFetch } from '@angular/common/http';
// Define a unique DI token for our library...
export const LIBRARYNAME_PROVIDER_REGISTERED = new InjectionToken<boolean>('LIBRARYNAME_PROVIDER_REGISTERED');
// Declare the exported library provider that will be consumed...
// This is what any consumer will call.
export const LIBRARYNAME_PROVIDERS: ApplicationConfig = {
providers: [...provideLibraryProviders()] // Invoke the factory function
};
// Factory function to determine ifon the providers shouldline.
This be registered...
// To make this work, we use a provider factory, whose lambda doesflattens the idempotencyreceived check for us, and registers things as required.
function provideLibraryProviders(): any[] {
return [
{
provide: 'LIBRARYNAME_CONDITIONAL_PROVIDERS',
useFactory: () => {
return runInInjectionContext(inject(EnvironmentInjector), () => {
// Attemptarray to retrieve our registration mark from DI...
const alreadyRegistered = inject(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 actual 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: 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.one-dimension.
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.