Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

If an Angular library holds a runtime reference to an optional component, that component’s code can stay in consumer bundles even when no application template ever renders it. The fix is a lightweight injection token: the library depends on a small abstract class, and the concrete component implements that class and registers itself through a provider. When nothing refers to the concrete component at runtime, the build can treat its code as unused and remove it. Angular’s guide on this pattern is written for library authors, because an application cannot fix a retention problem inside a dependency it imports. (Angular, “Optimizing client application size with lightweight injection tokens”)

Why an optional component stays in the bundle

Bundlers remove code that nothing references. TypeScript erases type-only references during compilation, so a component named only in a type position does not hold code in the output. A reference that must exist at runtime is different. If a library passes a component class to a content query or to inject(), the class is a runtime value, and the component, its template, and its styles travel with it. An application that never places that optional component in a template still ships it.

The trade-off is that the library’s public API is the cause. A parent component that queries for a concrete header class, for example, forces that class into every build that imports the parent, regardless of whether a consumer ever uses a header.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The lightweight token pattern

Angular’s documented approach separates the contract from the implementation. The parent depends on an abstract class, which exists at runtime but is small. The concrete component extends that class and provides itself under the abstract token with useExisting. The parent never names the concrete class.

  1. Declare an abstract class in the library that holds only the members the parent needs. Put required API methods or properties on this abstraction.
  2. Make the optional implementation component extend the abstract class.
  3. In the component’s providers array, register the abstract class with useExisting pointing at the component, so that the abstract token resolves to the existing component instance.
  4. In the parent, query or inject the abstract class instead of the concrete component.

A worked example

The following sketch uses a card component with an optional header. The abstract class is the only piece the parent references:

// card-header.token.ts (library)
export abstract class CardHeaderToken {
  abstract label: string;
}
// card-header.component.ts (library)
@Component({
  selector: 'lib-card-header',
  template: `<h2>{{ label }}</h2>`,
  providers: [{ provide: CardHeaderToken, useExisting: CardHeaderComponent }],
})
export class CardHeaderComponent extends CardHeaderToken {
  label = 'Untitled';
}
// card.component.ts (library)
@Component({
  selector: 'lib-card',
  template: `<ng-content />`,
})
export class CardComponent {
  header = contentChild(CardHeaderToken);
}

If an application never uses lib-card-header, nothing in the library names CardHeaderComponent at runtime. The retained code is the small abstract declaration and the parent. Angular’s own example follows this shape with a content query for an optional header. The size difference it produces depends on your application and build, and the guide does not quantify it.

Use InjectionToken for values with no runtime class

Interfaces, function types, and configuration objects have no runtime representation, so they cannot serve as a token. Use InjectionToken<T> instead. The generic parameter types the value that consumers receive.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
export const API_URL = new InjectionToken<string>('API_URL', {
  providedIn: 'root',
  factory: () => 'https://example.com/api',
});

A factory supplies a default, and a token with a factory can be provided at the root. Inside the factory, inject() can read other dependencies. (Angular, InjectionToken API; Angular, Defining dependency providers)

Token identity is object identity

A token is matched by the object reference, not by its description. Two calls to new InjectionToken<string>('API_URL') in different files create two unrelated tokens. A provider registered against one will not satisfy an injection of the other, and the injection fails with a NullInjectorError. Define each token once, export it, and import that same instance at both the provider and the injection site.

Choosing where the provider lives

Token design and provider scope are separate decisions. Angular resolves a dependency by walking up the injector hierarchy, so the place you register a provider determines which instance a component receives and how long it lives.

Registration Scope of the instance Tree-shaking Typical use
providedIn: 'root' One instance for the application Unused services can be removed from the bundle, per the Angular guide on defining providers Globally shared services and tokens
Component providers array One instance for that component and its children Not stated in the cited guidance for this case Isolated state, or overriding a service for one subtree
Route-level providers One instance for the route’s injector Not stated in the cited guidance for this case State that should live as long as a route

Use root provisioning when a service is shared and you want unused code removed. Use a narrower registration when each component needs its own instance or a different implementation. For the injector hierarchy and how lookups resolve, see Angular, Hierarchical injectors.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Calling inject() safely

inject() only works inside an injection context. In practice that means the constructor of a class Angular creates, field initializers of such classes, and provider or InjectionToken factories. Calling it from an arbitrary method, or from a callback that runs later, fails because no injector is active at that moment. Capture the value during construction and store it in a field if a method needs it later. Angular’s reference for the function lists the supported contexts. (Angular, inject API)

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting

  • NullInjectorError for a token you provided: the provider and the injection use different token instances. Check that both import from the same module and that the token was not created twice.
  • The abstract token resolves to nothing: the concrete component is not present in the template, so its useExisting provider is never instantiated. Query with an optional form and handle a null result, as the parent in the example does.
  • Bundle size did not change: something else still references the concrete class, such as an import used as a value, a barrel file that re-exports it, or an inject() call that names it. Remove those runtime references and rebuild.
  • A service is created more than once: it is registered in more than one component or route provider. Move the registration to the level where a single instance is intended.

What the official guidance establishes, and what it does not

Angular’s documentation describes the mechanism: a small abstract token allows the implementation to be removed when unused. It does not publish a bundle-size percentage, a benchmark, or a dated measurement for this pattern, and this article does not offer one. The saving depends on how much code the optional component contains and whether anything else references it. Measure the effect on your own build by comparing bundle output before and after the change. The technique targets bundle size and tree-shaking, not runtime speed, so do not present it as a performance gain in other respects.

Read the guidance as a rule about references. Keep the abstraction small, keep the concrete class out of the library’s runtime paths, and share one token instance everywhere it is used. (Angular, Optimizing client application size with lightweight injection tokens)

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.