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, inject } from '@angular/core';
import { provideHttpClient, withFetch } from '@angular/common/http';
// Define a unique DI token for our library...
export const LIB_LIBRARYNAME_PROVIDER_REGISTERED = new InjectionToken<boolean>('LIB_LIBRARYNAME_PROVIDER_REGISTERED');
export const LIB_LIBRARYNAME_PROVIDERS: ApplicationConfig =
{
providers:
[
// Check if the provider has already been registered...
// If found, return [].
// If not found, register our marker and return the providers...
// The ... at the beginning of this is a spread operator. Be sure to keep it in place.
...(inject(LIB_LIBRARYNAME_PROVIDER_REGISTERED, { optional: true }) ? [] :
[
// Register the Http Client provider...
provideHttpClient(withFetch()),
// Include any other 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, useValue: true }
])
]
};
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.