~3 min de lecture
InjectionToken tree-shakable : arrête de polluer ton bundle
Tu crées des InjectionToken pour injecter des configurations, des valeurs ou des APIs natives. Mais sais-tu que la
façon dont tu les déclares peut empêcher le tree-shaking et alourdir ton bundle inutilement ?
Le problème : le token classique
Voici comment beaucoup de développeurs créent leurs tokens :
// api-url.token.ts
export const API_URL = new InjectionToken<string>('API_URL');
Puis, quelque part dans l'application :
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
{provide: API_URL, useValue: 'https://api.example.com'}
]
};
Le problème ? Ce token est fourni dans le tableau providers. Même si aucun composant ou service ne l'utilise, il
sera inclus dans le bundle final. Le tree-shaker ne peut pas l'éliminer, car il y a une référence explicite dans la
configuration.
La solution : providedIn + factory
Tu peux rendre tes tokens tree-shakable en déclarant directement leur valeur :
// api-url.token.ts
export const API_URL = new InjectionToken<string>('API_URL', {
providedIn: 'root',
factory: () => 'https://api.example.com'
});
C'est tout. Plus besoin de l'ajouter dans providers. Angular le fournit automatiquement au niveau root, et **surtout
** : si personne ne l'injecte, il disparaît du bundle.
Comment ça marche ?
Le tree-shaking repose sur l'analyse statique des imports et des références. Quand tu mets un provider dans un tableau, tu crées une référence explicite — le bundler doit l'inclure.
Avec providedIn: 'root', il n'y a pas de référence dans un module ou une config. Angular résout la dépendance au
runtime via la factory. Si aucun code n'appelle inject(API_URL), le token et sa factory sont éliminés.
// ❌ Non tree-shakable : référence explicite dans providers
providers: [{provide: TOKEN, useValue: 'value'}]
// ✅ Tree-shakable : résolu à la demande
new InjectionToken('...', {providedIn: 'root', factory: () => 'value'})
Injecter des dépendances dans la factory
La vraie puissance arrive quand ta factory a besoin d'autres dépendances. Utilise inject() :
import {InjectionToken, inject} from '@angular/core';
import {DOCUMENT} from '@angular/common';
export const WINDOW = new InjectionToken<Window>('Window API', {
providedIn: 'root',
factory: () => inject(DOCUMENT).defaultView!
});
export const LOCAL_STORAGE = new InjectionToken<Storage>('LocalStorage API', {
providedIn: 'root',
factory: () => inject(WINDOW).localStorage
});
Tu peux chaîner les dépendances. Ici, LOCAL_STORAGE dépend de WINDOW, qui dépend de DOCUMENT. Tout reste
tree-shakable.
Cas d'usage concrets
Configuration applicative
type AppConfig = {
apiUrl: string;
debug: boolean;
maxRetries: number;
};
export const APP_CONFIG = new InjectionToken<AppConfig>('App Configuration', {
providedIn: 'root',
factory: () => ({
apiUrl: 'https://api.prod.example.com',
debug: false,
maxRetries: 3
})
});
Feature flags basés sur l'environnement
import {InjectionToken, inject, isDevMode} from '@angular/core';
export const ENABLE_ANALYTICS = new InjectionToken<boolean>('Analytics Flag', {
providedIn: 'root',
factory: () => !isDevMode()
});
Abstraction d'APIs navigateur
export const INTERSECTION_OBSERVER = new InjectionToken<typeof IntersectionObserver>(
'IntersectionObserver API',
{
providedIn: 'root',
factory: () => inject(WINDOW).IntersectionObserver
}
);
Ça facilite le mocking en tests et le SSR.
Le piège : instances multiples
Attention critique : utilise toujours la même instance du token.
// ❌ ERREUR : deux instances différentes
// tokens.ts
export const MY_TOKEN = new InjectionToken<string>('MyToken', {
providedIn: 'root',
factory: () => 'value'
});
// autre-fichier.ts
const MY_TOKEN = new InjectionToken<string>('MyToken'); // Nouvelle instance !
// inject(MY_TOKEN) → NullInjectorError 💥
Chaque appel à new InjectionToken() crée un token distinct, même avec la même description. Angular les traite
comme des clés différentes.
Solution : exporte et importe toujours le même token depuis un fichier dédié.
Surcharger un token tree-shakable
Tu peux toujours override un token tree-shakable dans providers si besoin :
// Le token avec sa valeur par défaut
export const API_URL = new InjectionToken<string>('API_URL', {
providedIn: 'root',
factory: () => 'https://api.prod.example.com'
});
// Override pour un environnement spécifique
export const appConfig: ApplicationConfig = {
providers: [
{provide: API_URL, useValue: 'https://api.staging.example.com'}
]
};
La factory sert de fallback. Si tu fournis explicitement le token, ta valeur prend le dessus.
Tests : TestBed.overrideProvider
Pour les tests, utilise overrideProvider avant la création du composant :
beforeEach(() => {
TestBed.overrideProvider(API_URL, {useValue: 'https://api.test.local'});
TestBed.configureTestingModule({
imports: [MyComponent]
});
});
Les options de providedIn
| Valeur | Comportement |
|---|---|
'root' |
Singleton au niveau application (recommandé) |
'platform' |
Partagé entre applications (cas rare, micro-frontends) |
null / non spécifié |
Doit être fourni manuellement |
'any' |
Déprécié — nouvelle instance par lazy module |
NgModule |
Déprécié — utilise 'root' à la place |
En résumé
| Approche | Tree-shakable | Quand l'utiliser |
|---|---|---|
providers: [{ provide, useValue }] |
❌ Non | Override explicite, valeurs dynamiques au bootstrap |
new InjectionToken(..., { providedIn, factory }) |
✅ Oui | Valeurs par défaut, configurations, APIs |
La règle : déclare tes tokens avec providedIn: 'root' et une factory par défaut. Override uniquement quand c'est
nécessaire.
// ✅ Pattern recommandé
export const MY_TOKEN = new InjectionToken<MyType>('Description', {
providedIn: 'root',
factory: () => defaultValue
});
Tes tokens deviennent tree-shakable, testables, et tu n'as plus à les déclarer dans chaque module. Moins de boilerplate, bundle plus léger.