| |

Angular 57 🅰️ Provider Recipes

In Angular 54 through 56, the DI’s foundation was covered: the injector hierarchy, the injection tokens, the providers, and the hierarchical resolution. This chapter is the practical companion — the recipes. A provider is the recipe that tells the injector what to create for a token. The four forms — useClass, useValue, useFactory, useExisting — cover the basics, but the real applications need more: an interface mapped to an implementation, a configurable service with options, a service that depends on the other services, a value that must be computed at the startup, a multi-provider that collects into the array, a provider that is scoped to the component. The recipes in this chapter are the patterns that appear in every Angular application, and each has a specific use.

Key point: A provider is an object with a provide key (the token) and one of the use* keys (the recipe). The useClass instantiates the class, the useValue returns the value, the useFactory calls the factory, and the useExisting returns the alias. The multi-providers (multi: true) collect the multiple values into an array. The providedIn on the @Injectable makes the provider tree-shakable, and the providers on the component, the directive, or the route scopes it. The provideX function is the modern API, and the makeEnvironmentProviders groups the providers for the feature. The InjectionToken<T> is the typed token, and the @Optional, the @Self, the @SkipSelf, and the @Host are the modifiers.


The recipe 1 — the class to the interface

The interface cannot be the token (the interface does not exist at runtime), and the InjectionToken is the solution. The token is the interface’s, and the provider maps it to the implementation.

export interface Logger {
  log(message: string): void;
}

export const LOGGER = new InjectionToken<Logger>('LOGGER');

@Injectable()
export class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log('[console]', message);
  }
}

const providers: Provider[] = [
  { provide: LOGGER, useClass: ConsoleLogger },
];

The LOGGER is the token, and the ConsoleLogger is the implementation. The consumer injects the LOGGER, and the injector returns the ConsoleLogger.

Why the interface’s token. The interface is the compile-time’s, and the runtime does not have it. The InjectionToken is the runtime’s, and the token is the key. The two are the pair, and the pair is the pattern.

Why the useClass is the swap. The useClass is the swap, and the swap is the implementation’s. The LOGGER‘s implementation can be the ConsoleLogger, the FileLogger, or the NullLogger, and the token is the same. The swap is the flexibility, and the flexibility is the value.

Why the token is the InjectionToken. The InjectionToken<Logger> is the typed, and the inject(LOGGER) is the Logger. The type is the safety, and the token is the key. The two are the pair, and the pair is the modern.

Why the test can swap the implementation. The test provides the mock, and the component’s inject(LOGGER) resolves the mock. The production provides the real, and the test provides the fake. The two are the pair, and the pair is the test’s.

The useClass with the dependencies. The useClass‘s class can have the dependencies, and the injector resolves them.

@Injectable()
export class FileLogger implements Logger {
  constructor(private readonly config: AppConfig) {}
  log(message: string): void {
    writeToFile(this.config.logFile, message);
  }
}

The FileLogger depends on the AppConfig, and the injector resolves the AppConfig from the hierarchy. The two are the pair, and the pair is the DI’s.

Why the useClass‘s dependencies matter. The useClass‘s class is the DI’s, and the dependencies are the injector’s. The class declares the dependencies, and the injector provides them. The two are the pair, and the pair is the DI.

Why the useClass should be the interface’s. The useClass is the interface’s, and the interface is the contract. The implementation is the swap’s, and the swap is the flexibility. The two are the pair, and the pair is the design.


The recipe 2 — the value

The useValue provides the value, and the value is the constant. The configuration, the feature flags, the API’s URL are the values.

export const API_URL = new InjectionToken<string>('API_URL');
export const FEATURE_FLAGS = new InjectionToken<FeatureFlags>('FEATURE_FLAGS');

const providers: Provider[] = [
  { provide: API_URL, useValue: 'https://api.example.com' },
  {
    provide: FEATURE_FLAGS,
    useValue: { darkMode: true, betaFeatures: false },
  },
];

The API_URL is the string, and the FEATURE_FLAGS is the object. The two are the values, and the values are the constants.

Why the useValue is the constant. The useValue is the value, and the value is the same for the consumers. The value is the configuration, and the configuration is the constant. The two are the pair, and the pair is the config.

Why the useValue‘s object should be the readonly. The object is the shared, and the shared is the mutation’s risk. The as const or the Readonly is the safety, and the safety is the value.

export const FEATURE_FLAGS = new InjectionToken<Readonly<FeatureFlags>>('FEATURE_FLAGS');

const providers: Provider[] = [
  {
    provide: FEATURE_FLAGS,
    useValue: Object.freeze({ darkMode: true, betaFeatures: false }),
  },
];

The Readonly<FeatureFlags> is the type, and the Object.freeze is the runtime. The two are the pair, and the pair is the safety.

Why the readonly matters. The shared’s object is the mutation’s risk, and the mutation is the bug. The readonly is the prevention, and the prevention is the safety. The two are the pair, and the pair is the discipline.

Why the useValue should be the simple. The useValue is the simple, and the simple is the value. The complex’s logic is the useFactory‘s, and the factory is the dynamic. The two are the choice, and the choice is the value’s.

Why the useValue can be the test’s. The test provides the mock’s value, and the component’s inject(API_URL) resolves the mock. The production provides the real, and the test provides the fake. The two are the pair, and the pair is the test’s.

Why the useValue matters. The useValue is the constant, and the constant is the config. The two are the pair, and the pair is the pattern. The useValue is the common, and the common is the config.


The recipe 3 — the factory

The useFactory calls the factory, and the factory returns the instance. The factory is the dynamic, and the dynamic is the runtime’s.

export const LOGGER = new InjectionToken<Logger>('LOGGER');

const providers: Provider[] = [
  {
    provide: LOGGER,
    useFactory: () => {
      const config = inject(AppConfig);
      return config.debug ? new ConsoleLogger() : new FileLogger(config);
    },
  },
];

The useFactory calls the function, and the function returns the Logger. The inject(AppConfig) is the dependency, and the dependency is the factory’s.

Why the factory is the dynamic. The factory is the function, and the function is the runtime’s. The conditional is the factory’s, and the conditional is the dynamic. The two are the pair, and the pair is the runtime.

Why the inject() in the factory. The inject() is the modern, and the modern is the concise. The deps array is the legacy, and the legacy is the verbose. The two are the equivalent, and the equivalent is the choice.

// The legacy:
{
  provide: LOGGER,
  useFactory: (config: AppConfig) => config.debug ? new ConsoleLogger() : new FileLogger(config),
  deps: [AppConfig],
}

// The modern:
{
  provide: LOGGER,
  useFactory: () => {
    const config = inject(AppConfig);
    return config.debug ? new ConsoleLogger() : new FileLogger(config);
  },
}

The two are the equivalent, and the inject() is the modern. The two are the choice, and the choice is the style.

Why the deps array matters. The deps array is the legacy, and the array is the explicit. The inject() is the modern, and the inject() is the implicit. The two are the equivalent, and the equivalent is the modern.

Why the factory is the configurable. The factory is the configurable, and the configurable is the parameterized. The factory’s closure is the config, and the config is the runtime’s. The two are the pair, and the pair is the pattern.

Why the factory can be the async. The factory can return the Promise, and the APP_INITIALIZER is the async’s.

const providers: Provider[] = [
  {
    provide: APP_INITIALIZER,
    useFactory: () => {
      const config = inject(ConfigService);
      return () => config.load();
    },
    multi: true,
  },
];

The factory returns the function, and the function returns the Promise. The APP_INITIALIZER is the async’s, and the async is the startup’s.

Why the async’s factory matters. The async’s factory is the startup’s, and the startup is the config’s. The APP_INITIALIZER is the framework’s, and the framework is the modern. The two are the pair, and the pair is the config.

Why the factory should be the pure. The factory should be the pure, and the pure is the predictable. The side effects are the effect’s, and the effect is the separate. The two are the pair, and the pair is the discipline.


The recipe 4 — the alias

The useExisting returns the alias, and the alias is the same instance.

const providers: Provider[] = [
  { provide: Logger, useClass: ConsoleLogger },
  { provide: OldLogger, useExisting: Logger },
];

The OldLogger is the alias for the Logger, and the two resolve to the same ConsoleLogger‘s instance. The alias is the backward compatibility, and the compatibility is the migration’s.

Why the alias is the same instance. The useExisting returns the existing, and the existing is the same. The two tokens resolve to the same, and the same is the shared. The two are the pair, and the pair is the alias.

Why the useClass is the different. The useClass creates the new, and the new is the separate. The two tokens resolve to the different, and the different is the separate. The two are the pair, and the pair is the difference.

const providers: Provider[] = [
  { provide: Logger, useClass: ConsoleLogger },
  { provide: OldLogger, useClass: ConsoleLogger },  // the new instance
];

The OldLogger‘s useClass creates the new, and the new is the separate. The two are the pair, and the pair is the difference.

Why the alias matters. The alias is the migration’s, and the migration is the refactor’s. The old’s token is the alias, and the new’s is the primary. The two are the pair, and the pair is the transition.

Why the alias can be the interface’s. The alias can be the interface’s, and the interface is the contract.

const providers: Provider[] = [
  { provide: LOGGER, useClass: ConsoleLogger },
  { provide: LEGACY_LOGGER, useExisting: LOGGER },
];

The LEGACY_LOGGER is the alias for the LOGGER, and the two resolve to the same. The alias is the interface’s, and the interface is the contract. The two are the pair, and the pair is the pattern.

Why the alias should be the deliberate. The alias is the deliberate, and the deliberate is the design. The alias is the migration’s, and the migration is the temporary. The two are the pair, and the pair is the transition.

Why the alias can be the deprecated. The alias can be the deprecated, and the deprecated is the marker. The @deprecated is the JSDoc, and the JSDoc is the editor’s. The two are the pair, and the pair is the migration.


The recipe 5 — the multi-provider

The multi-provider collects the multiple values into the array. The multi: true is the marker, and the array is the result.

export const VALIDATORS = new InjectionToken<Validator[]>('VALIDATORS');

const providers: Provider[] = [
  { provide: VALIDATORS, useClass: EmailValidator, multi: true },
  { provide: VALIDATORS, useClass: PhoneValidator, multi: true },
  { provide: VALIDATORS, useClass: RequiredValidator, multi: true },
];

The VALIDATORS is the token, and the three providers add the three validators. The inject(VALIDATORS) returns the array of the three.

Why the multi is the collection. The multi is the collection, and the collection is the array. The multiple providers with the same token and the multi: true collect into the array. The two are the pair, and the pair is the plugin.

Why the multi’s order matters. The multi’s order is the declaration’s, and the declaration’s is the array’s. The validators are the order’s, and the order is the execution’s. The two are the pair, and the pair is the sequence.

Why the multi is the framework’s. The multi is the framework’s, and the framework is the APP_INITIALIZER, the HTTP_INTERCEPTORS, the ROUTES, and the ENVIRONMENT_INITIALIZER. The multi is the framework’s pattern, and the pattern is the plugin. The two are the pair, and the pair is the composition.

Why the multi’s useValue matters. The multi’s useValue is the value’s, and the value is the array’s element.

const providers: Provider[] = [
  { provide: FEATURE_FLAGS, useValue: { name: 'darkMode', enabled: true }, multi: true },
  { provide: FEATURE_FLAGS, useValue: { name: 'beta', enabled: false }, multi: true },
];

The two useValues add the two objects, and the inject(FEATURE_FLAGS) returns the array of the two. The two are the pair, and the pair is the collection.

Why the multi’s useFactory matters. The multi’s useFactory is the factory’s, and the factory is the element’s.

const providers: Provider[] = [
  {
    provide: VALIDATORS,
    useFactory: () => inject(AppConfig).strict ? [new StrictValidator()] : [],
    multi: true,
  },
];

The factory returns the array, and the array is the spread into the multi’s. The two are the pair, and the pair is the dynamic’s.

Why the multi’s useFactory‘s array matters. The multi’s useFactory returns the array, and the array is the flattened. The multi: true is the flatten, and the flatten is the collection. The two are the pair, and the pair is the pattern.

Why the multi should be the known. The multi is the known, and the known is the fluency. The APP_INITIALIZER, the HTTP_INTERCEPTORS, the ROUTES are the multi’s, and the multi’s is the framework. The two are the pair, and the pair is the common.


The recipe 6 — the component’s provider

The component’s providers array creates the element injector, and the providers are the component’s subtree’s.

@Component({
  selector: 'app-feature',
  standalone: true,
  providers: [FeatureService, { provide: LOGGER, useClass: FeatureLogger }],
  template: `<app-child />`,
})
export class FeatureComponent {}

The FeatureService is the component’s, and the LOGGER‘s override is the component’s. The subtree’s components use the component’s providers, and the component’s is the scope.

Why the component’s provider is the scope. The component’s provider is the scope, and the scope is the subtree’s. The component’s and the subtree’s share the instance, and the sharing is the scope. The two are the pair, and the pair is the component’s.

Why the component’s provider’s lifetime is the component’s. The component’s provider’s lifetime is the component’s, and the component’s is the create and destroy. The service is created with the component, and destroyed with the component. The two are the pair, and the pair is the lifetime.

Why the component’s provider is the per-instance. The component’s provider is the per-instance, and the per-instance is the two components’. The two FeatureComponents have the two FeatureServices, and the two are the separate. The per-instance is the scope, and the scope is the component’s.

Why the component’s provider can be the override. The component’s provider overrides the environment’s, and the override is the local. The component can override the service for its subtree, and the subtree’s components use the override. The two are the pair, and the pair is the override.

Why the component’s provider should be the sparing. The component’s provider is the per-instance, and the per-instance is the memory. The application’s services should be the root’s, and the root’s is the singleton. The component’s provider is the specific, and the specific is the use.

Why the component’s provider matters. The component’s provider is the local, and the local is the state’s. The component’s service holds the component’s state, and the state is the component’s. The two are the pair, and the pair is the state.


The recipe 7 — the route’s provider

The route’s providers array creates the route’s environment injector, and the providers are the route’s subtree’s.

const routes: Routes = [
  {
    path: 'admin',
    providers: [AdminService, { provide: LOGGER, useClass: AdminLogger }],
    children: [
      { path: '', component: AdminComponent },
    ],
  },
];

The AdminService is the route’s, and the LOGGER‘s override is the route’s. The route’s components use the route’s providers, and the route’s is the scope.

Why the route’s provider is the lazy’s. The route’s provider is the lazy’s, and the lazy is the module’s. The route’s injector is created when the route is loaded, and the route’s service is the route’s. The two are the pair, and the pair is the lazy.

Why the route’s provider’s lifetime is the route’s. The route’s provider’s lifetime is the route’s, and the route’s is the load and unload. The service is created with the route, and destroyed with the route. The two are the pair, and the pair is the cleanup.

Why the route’s provider is the feature’s. The route’s provider is the feature’s, and the feature’s is the scope. The route’s service is the feature’s, and the feature’s is the natural. The two are the pair, and the pair is the design.

Why the route’s provider matters for the singleton. The route’s provider is the route’s singleton, and the route’s is the feature’s. The route’s components share the service, and the sharing is the scope. The two are the pair, and the pair is the singleton.

Why the route’s provider should be the feature’s. The route’s provider is the feature’s, and the feature’s is the scope. The route’s service is the feature’s, and the feature’s is the natural. The two are the pair, and the pair is the design.

Why the route’s provider can be the resolved. The route’s resolve is the data’s, and the data is the route’s.

const routes: Routes = [
  {
    path: 'user/:id',
    resolve: { user: UserResolver },
    component: UserComponent,
  },
];

The UserResolver resolves the data, and the data is the route’s. The resolver is the route’s, and the route’s is the data’s. The two are the pair, and the pair is the pattern.


The recipe 8 — the provideX function

The provideX function is the modern API, and the function groups the providers for the feature.

export function provideFeature(config: FeatureConfig): EnvironmentProviders {
  return makeEnvironmentProviders([
    { provide: FEATURE_CONFIG, useValue: config },
    FeatureService,
    {
      provide: ENVIRONMENT_INITIALIZER,
      useValue: () => inject(FeatureService).init(),
      multi: true,
    },
  ]);
}

The provideFeature returns the EnvironmentProviders, and the consumer uses the providers: [provideFeature(config)]. The function is the API, and the API is the feature’s.

Why the provideX is the modern. The provideX is the modern, and the modern is the Angular 15+. The ModuleWithProviders is the legacy, and the legacy is the deprecated. The two are the equivalent, and the provideX is the modern.

Why the makeEnvironmentProviders matters. The makeEnvironmentProviders groups the providers, and the group is the EnvironmentProviders. The type is the opaque, and the opaque is the safe. The two are the pair, and the pair is the modern.

Why the EnvironmentProviders cannot be the spread. The EnvironmentProviders is the opaque, and the opaque cannot be the spread. The function is the group, and the group is the single. The two are the pair, and the pair is the modern.

// Wrong:
providers: [...provideFeature(config)]  // ❌

// Right:
providers: [provideFeature(config)]  // ✅

The ...provideFeature(config) is the error, and the error is the opaque. The provideFeature(config) is the correct, and the correct is the single. The two are the pair, and the pair is the modern.

Why the provideX‘s config matters. The provideX‘s config is the parameter, and the parameter is the feature’s. The provideFeature(config) is the configurable, and the configurable is the feature’s. The two are the pair, and the pair is the config.

Why the provideX should be the library’s. The provideX is the library’s, and the library’s is the API. The consumer uses the provideX, and the provideX is the encapsulation. The two are the pair, and the pair is the library.

Why the provideX matters. The provideX is the modern, and the modern is the API. The function is the group, and the group is the single. The two are the pair, and the pair is the pattern.


The recipe 9 — the InjectionToken‘s factory

The InjectionToken can have the providedIn and the factory, and the self-provided token does not need the providers array’s entry.

export const DEFAULT_TIMEOUT = new InjectionToken<number>('DEFAULT_TIMEOUT', {
  providedIn: 'root',
  factory: () => 30000,
});

The DEFAULT_TIMEOUT is the self-provided, and the inject(DEFAULT_TIMEOUT) returns the 30000. The token is the self-provided, and the self-provided is the tree-shakable.

Why the self-provided matters. The self-provided is the tree-shakable, and the tree-shakable is the bundle’s. The unused’s token is the removed, and the removed is the optimization. The two are the pair, and the pair is the modern.

Why the token’s factory is the default. The token’s factory is the default, and the default is the fallback. The test can override the token, and the override is the test’s. The two are the pair, and the pair is the flexibility.

Why the token’s providedIn can be the module. The token’s providedIn can be the module’s, and the module’s is the scope.

export const FEATURE_CONFIG = new InjectionToken<FeatureConfig>('FEATURE_CONFIG', {
  providedIn: 'root',
  factory: () => ({ enabled: false }),
});

The FEATURE_CONFIG‘s providedIn is the root’s, and the factory is the default. The two are the pair, and the pair is the self-provided.

Why the token’s factory can use the inject. The token’s factory can use the inject, and the inject is the modern.

export const LOGGER = new InjectionToken<Logger>('LOGGER', {
  providedIn: 'root',
  factory: () => {
    const config = inject(AppConfig);
    return config.debug ? new ConsoleLogger() : new FileLogger(config);
  },
});

The factory uses the inject(AppConfig), and the AppConfig is the dependency. The factory is the dynamic, and the dynamic is the modern.

Why the token’s factory’s inject matters. The inject‘s factory is the modern, and the modern is the dependency. The factory is the dynamic, and the dynamic is the config. The two are the pair, and the pair is the pattern.

Why the token’s factory should be the preferred. The token’s factory is the preferred, and the preferred is the self-contained. The providers array’s entry is the explicit, and the explicit is the alternative. The two are the choice, and the choice is the design.


The recipe 10 — the APP_INITIALIZER

The APP_INITIALIZER runs the function at the application’s startup, and the function can return the Promise for the async.

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: () => {
        const config = inject(ConfigService);
        return () => config.load();
      },
      multi: true,
    },
  ],
};

The APP_INITIALIZER runs the function, and the function returns the () => config.load(). The config.load() returns the Promise, and the application waits for the resolve.

Why the APP_INITIALIZER matters. The APP_INITIALIZER is the startup’s, and the startup is the config’s. The application waits for the initializer, and the wait is the safety. The two are the pair, and the pair is the startup.

Why the APP_INITIALIZER‘s factory returns the function. The factory returns the function, and the function returns the Promise. The two are the nested, and the nested is the pattern. The factory is the inject’s, and the function is the runtime’s.

Why the APP_INITIALIZER‘s inject matters. The inject is the factory’s, and the factory is the modern. The deps array is the legacy, and the legacy is the verbose. The two are the equivalent, and the inject is the modern.

Why the APP_INITIALIZER can be the multiple. The APP_INITIALIZER can be the multiple, and the multiple is the multi: true. The multiple initializers run in the order, and the order is the declaration’s. The two are the pair, and the pair is the sequence.

Why the APP_INITIALIZER should be the sparing. The APP_INITIALIZER blocks the startup, and the block is the delay. The critical’s config is the initializer’s, and the non-critical is the lazy. The two are the pair, and the pair is the performance.

Why the APP_INITIALIZER matters. The APP_INITIALIZER is the startup’s, and the startup is the application’s. The config’s load, the auth’s check, the feature flag’s fetch are the initializer’s. The two are the pair, and the pair is the common.


Complete Example Session

import {
  Injectable, InjectionToken, Provider, EnvironmentProviders,
  makeEnvironmentProviders, inject, APP_INITIALIZER,
  Component, Optional, Self, SkipSelf, Host,
} from '@angular/core';

// ============================================
// PART 1: THE INTERFACE AND THE TOKEN
// ============================================

export interface Logger {
  log(message: string): void;
}

export const LOGGER = new InjectionToken<Logger>('LOGGER');

// ============================================
// PART 2: THE IMPLEMENTATIONS
// ============================================

@Injectable()
export class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log('[console]', message);
  }
}

@Injectable()
export class FileLogger implements Logger {
  constructor(private readonly config: AppConfig) {}
  log(message: string): void {
    writeToFile(this.config.logFile, message);
  }
}

// ============================================
// PART 3: THE PROVIDER RECIPES
// ============================================

const providers: Provider[] = [
  // The class to the interface
  { provide: LOGGER, useClass: ConsoleLogger },

  // The value
  { provide: API_URL, useValue: 'https://api.example.com' },
  { provide: FEATURE_FLAGS, useValue: Object.freeze({ darkMode: true }) },

  // The factory
  {
    provide: LOGGER,
    useFactory: () => {
      const config = inject(AppConfig);
      return config.debug ? new ConsoleLogger() : new FileLogger(config);
    },
  },

  // The alias
  { provide: OldLogger, useExisting: LOGGER },

  // The multi
  { provide: VALIDATORS, useClass: EmailValidator, multi: true },
  { provide: VALIDATORS, useClass: PhoneValidator, multi: true },
];

// ============================================
// PART 4: THE COMPONENT'S PROVIDER
// ============================================

@Component({
  selector: 'app-feature',
  standalone: true,
  providers: [
    FeatureService,
    { provide: LOGGER, useClass: FeatureLogger },
  ],
  template: `<app-child />`,
})
export class FeatureComponent {}

// ============================================
// PART 5: THE ROUTE'S PROVIDER
// ============================================

const routes: Routes = [
  {
    path: 'admin',
    providers: [AdminService, { provide: LOGGER, useClass: AdminLogger }],
    children: [{ path: '', component: AdminComponent }],
  },
];

// ============================================
// PART 6: THE PROVIDE X FUNCTION
// ============================================

export function provideFeature(config: FeatureConfig): EnvironmentProviders {
  return makeEnvironmentProviders([
    { provide: FEATURE_CONFIG, useValue: config },
    FeatureService,
    {
      provide: ENVIRONMENT_INITIALIZER,
      useValue: () => inject(FeatureService).init(),
      multi: true,
    },
  ]);
}

// The consumer:
// providers: [provideFeature({ enabled: true })]

// ============================================
// PART 7: THE INJECTION TOKEN'S FACTORY
// ============================================

export const DEFAULT_TIMEOUT = new InjectionToken<number>('DEFAULT_TIMEOUT', {
  providedIn: 'root',
  factory: () => 30000,
});

// ============================================
// PART 8: THE APP INITIALIZER
// ============================================

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: () => {
        const config = inject(ConfigService);
        return () => config.load();
      },
      multi: true,
    },
  ],
};

// ============================================
// PART 9: THE MODIFIERS
// ============================================

@Component({ selector: 'app-child', standalone: true, template: `` })
export class ChildComponent {
  private readonly self = inject(LOGGER, { self: true });
  private readonly skipSelf = inject(LOGGER, { skipSelf: true });
  private readonly optional = inject(LOGGER, { optional: true });
}

@Directive({ selector: '[appHighlight]' })
export class HighlightDirective {
  private readonly host = inject(LOGGER, { host: true });
}

// ============================================
// PART 10: WHAT NOT TO DO
// ============================================

// Don't use the string token
{ provide: 'API_URL', useValue: '...' }                        // ⚠️

// Don't forget the multi
{ provide: APP_INITIALIZER, useFactory: () => ... }            // ⚠️

// Don't spread the EnvironmentProviders
providers: [...provideFeature(config)]                         // ⚠️

// Don't use the useClass for the alias
{ provide: OldLogger, useClass: ConsoleLogger }  // the new    // ⚠️

// Don't forget the token's type
new InjectionToken('LOGGER')  // the untyped                    // ⚠️

// Don't use the component's provider for the singleton
// The component's provider is the per-instance.              // ⚠️

The ten parts cover the interface and the token, the implementations, the provider recipes, the component’s provider, the route’s provider, the provideX function, the token’s factory, the APP_INITIALIZER, the modifiers, and the anti-patterns.


Quick Reference

The Four use* Types

The typeThe purpose
The useClassThe class instance
The useValueThe value
The useFactoryThe factory
The useExistingThe alias

The Token’s Types

The tokenThe use
The classThe service
The InjectionToken<T>The interface, the config
The stringThe legacy

The Token’s Options

The optionThe purpose
The providedInThe scope
The factoryThe default

The Multi-Providers

The tokenThe framework’s
The APP_INITIALIZERThe startup
The ENVIRONMENT_INITIALIZERThe env’s setup
The HTTP_INTERCEPTORSThe HTTP
The ROUTESThe routes

The Provider’s Levels

The levelThe scope
The rootThe application
The routeThe route’s subtree
The componentThe component’s subtree

The Modifiers

The modifierThe inject’s option
The @SelfThe { self: true }
The @SkipSelfThe { skipSelf: true }
The @HostThe { host: true }
The @OptionalThe { optional: true }

Best Practices

✅ Do This:

// Use the InjectionToken for the interface
export const LOGGER = new InjectionToken<Logger>('LOGGER');    // ✅

// Use the useClass for the interface's implementation
{ provide: LOGGER, useClass: ConsoleLogger }                   // ✅

// Use the useValue for the constant
{ provide: API_URL, useValue: 'https://api.example.com' }      // ✅

// Use the useFactory for the dynamic
{ provide: LOGGER, useFactory: () => inject(AppConfig).debug ? ... : ... } // ✅

// Use the useExisting for the alias
{ provide: OldLogger, useExisting: LOGGER }                    // ✅

// Use the multi for the collection
{ provide: APP_INITIALIZER, useFactory: () => ..., multi: true } // ✅

// Use the provideX function for the feature
export function provideFeature(config: FeatureConfig): EnvironmentProviders {
  return makeEnvironmentProviders([...]);
}                                                              // ✅

// Use the self-provided token
new InjectionToken<number>('TIMEOUT', { providedIn: 'root', factory: () => 30000 }) // ✅

❌ Don’t Do This:

// Don't use the string token
{ provide: 'API_URL', useValue: '...' }                        // ⚠️

// Don't forget the multi
{ provide: APP_INITIALIZER, useFactory: () => ... }            // ⚠️

// Don't spread the EnvironmentProviders
providers: [...provideFeature(config)]                         // ⚠️

// Don't use the useClass for the alias
{ provide: OldLogger, useClass: ConsoleLogger }                // ⚠️

// Don't forget the token's type
new InjectionToken('LOGGER')                                   // ⚠️

// Don't use the component's provider for the singleton
// The component's provider is the per-instance.              // ⚠️

Common Pitfalls

PitfallProblemSolution
The string tokenThe collisionThe InjectionToken
The missing multiThe array’s notThe multi: true
The spread’s EnvironmentProvidersThe opaqueThe function
The useClass for the aliasThe new instanceThe useExisting
The token’s untypedThe anyThe generic
The component’s singletonThe per-instanceThe providedIn: 'root'
The APP_INITIALIZER‘s blockThe startup’s delayThe sparing
The factory’s side effectThe impureThe pure

Real-World Examples

1. The interface’s token

export const LOGGER = new InjectionToken<Logger>('LOGGER');

2. The useClass

{ provide: LOGGER, useClass: ConsoleLogger }

3. The useValue

{ provide: API_URL, useValue: 'https://api.example.com' }

4. The useFactory

{ provide: LOGGER, useFactory: () => inject(AppConfig).debug ? new ConsoleLogger() : new FileLogger() }

5. The useExisting

{ provide: OldLogger, useExisting: LOGGER }

6. The multi

{ provide: APP_INITIALIZER, useFactory: () => ..., multi: true }

7. The component’s provider

@Component({ providers: [FeatureService] })

8. The route’s provider

{ path: 'admin', providers: [AdminService], children: [...] }

9. The provideX function

export function provideFeature(config: FeatureConfig): EnvironmentProviders {
  return makeEnvironmentProviders([...]);
}

10. The self-provided token

new InjectionToken<number>('TIMEOUT', { providedIn: 'root', factory: () => 30000 })

Visual: The Provider’s Structure

┌──────────────────────────────────────────────────────────┐
│  { provide: TOKEN, useClass: Class }                     │
│  │         │        │                                    │
│  │         │        └── the class instance               │
│  │         └── the token                                 │
│  └── the provider object                                 │
│                                                          │
│  { provide: TOKEN, useValue: value }                     │
│  { provide: TOKEN, useFactory: () => ... }               │
│  { provide: TOKEN, useExisting: OTHER }                  │
│  { provide: TOKEN, useClass: Class, multi: true }        │
│                                                          │
│  The provide is the token, and the use* is the recipe.   │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The useClass vs the useExisting

┌──────────────────────────────────────────────────────────┐
│  THE useClass                                            │
│    { provide: Logger, useClass: ConsoleLogger }          │
│    { provide: OldLogger, useClass: ConsoleLogger }       │
│       │                                                  │
│       └── The two resolve to the DIFFERENT instances.    │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  THE useExisting                                         │
│    { provide: Logger, useClass: ConsoleLogger }          │
│    { provide: OldLogger, useExisting: Logger }           │
│       │                                                  │
│       └── The two resolve to the SAME instance.          │
│                                                          │
│  The useClass creates the new, and the useExisting       │
│  returns the existing.                                   │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Multi-Provider

┌──────────────────────────────────────────────────────────┐
│  { provide: VALIDATORS, useClass: EmailValidator, multi: true }│
│  { provide: VALIDATORS, useClass: PhoneValidator, multi: true }│
│  { provide: VALIDATORS, useClass: RequiredValidator, multi: true }│
│       │                                                  │
│       ▼                                                  │
│  inject(VALIDATORS)  → [EmailValidator, PhoneValidator, RequiredValidator]│
│                                                          │
│  The multiple providers collect into the array.          │
│  The order is the declaration's.                         │
│                                                          │
│  THE FRAMEWORK'S TOKENS                                  │
│    APP_INITIALIZER        → the startup                  │
│    ENVIRONMENT_INITIALIZER → the env's setup             │
│    HTTP_INTERCEPTORS      → the HTTP                     │
│    ROUTES                 → the routes                   │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Provider’s Levels

┌──────────────────────────────────────────────────────────┐
│  THE ROOT                                                │
│    @Injectable({ providedIn: 'root' })                   │
│    The application's singleton.                          │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  THE ROUTE                                               │
│    { path: 'admin', providers: [AdminService] }          │
│    The route's subtree, the lazy's.                      │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  THE COMPONENT                                           │
│    @Component({ providers: [FeatureService] })           │
│    The component's subtree, the per-instance.            │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  THE RESOLUTION                                          │
│    The component first, then the route, then the root.   │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The provideX Function

┌──────────────────────────────────────────────────────────┐
│  export function provideFeature(config: FeatureConfig): EnvironmentProviders {│
│    return makeEnvironmentProviders([                     │
│      { provide: FEATURE_CONFIG, useValue: config },      │
│      FeatureService,                                     │
│      {                                                   │
│        provide: ENVIRONMENT_INITIALIZER,                 │
│        useValue: () => inject(FeatureService).init(),    │
│        multi: true,                                      │
│      },                                                  │
│    ]);                                                   │
│  }                                                       │
│                                                          │
│  THE CONSUMER                                            │
│    providers: [provideFeature({ enabled: true })]        │
│                                                          │
│  The provideX is the modern's API, and the               │
│  makeEnvironmentProviders is the group.                  │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Token’s Factory

┌──────────────────────────────────────────────────────────┐
│  export const DEFAULT_TIMEOUT = new InjectionToken<number>('DEFAULT_TIMEOUT', {│
│    providedIn: 'root',                                   │
│    factory: () => 30000,                                 │
│  });                                                     │
│                                                          │
│  inject(DEFAULT_TIMEOUT)  → 30000                        │
│                                                          │
│  THE SELF-PROVIDED                                       │
│    The token provides itself, and the providers array's  │
│    entry is not needed.                                  │
│                                                          │
│  THE OVERRIDE                                            │
│    The test can provide the token, and the override is   │
│    the test's.                                           │
│                                                          │
└──────────────────────────────────────────────────────────┘

Summary

The recipeThe form
The class to the interfaceThe useClass with the InjectionToken
The valueThe useValue
The factoryThe useFactory with the inject
The aliasThe useExisting
The multiThe multi: true
The component’sThe @Component({ providers })
The route’sThe route’s providers
The provideXThe makeEnvironmentProviders
The token’s factoryThe providedIn + the factory
The APP_INITIALIZERThe multi: true with the function

Key takeaways:

  • The useClass maps the interface’s token to the implementation — the InjectionToken is the token, and the class is the swap
  • The useValue provides the constant — the config, the feature flags, and the API’s URL are the values, and the Readonly is the safety
  • The useFactory provides the dynamic — the factory calls the inject, and the conditional is the runtime’s
  • The useExisting provides the alias — the two tokens resolve to the same instance, and the alias is the migration’s
  • The multi: true collects into the array — the APP_INITIALIZER, the HTTP_INTERCEPTORS, and the ROUTES are the framework’s multi-providers
  • The component’s providers creates the element injector — the scope is the component’s subtree, and the lifetime is the component’s
  • The route’s providers creates the environment injector — the scope is the route’s subtree, and the lazy’s is the cleanup
  • The provideX function is the modern API — the makeEnvironmentProviders groups the providers, and the EnvironmentProviders is the opaque
  • The token’s providedIn and factory is the self-provided — the tree-shakable, and the test can override it
  • The APP_INITIALIZER runs at the startup — the function returns the Promise, and the application waits

Remember: The provider is the recipe, and the token is the key. The four use* types are the forms, and the multi is the collection. The component’s and the route’s providers scope the services, and the provideX is the modern’s API. The token’s factory is the self-provided, and the APP_INITIALIZER is the startup’s. The recipes are the vocabulary, and the vocabulary is the fluency.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!