Tokens have runtime identity
Injection looks up a key and returns the provider-associated value. A class can serve as a token because it exists at runtime. A TypeScript interface disappears from JavaScript output and cannot by itself serve as that key.
For configurations or contracts without a concrete class, declare and export an InjectionToken instance. Its constructor description helps debugging but does not unify objects: two new InjectionToken calls with the same description create different tokens.
Import the same token in producers and consumers. The generic type helps typing without replacing object identity.
Choose the provider strategy
useClass asks the system to create an instance of the specified class. useValue supplies an existing value such as a configuration object; supplying a class as a value does not instantiate it. useFactory calculates the value and can receive dependencies declared in deps in parameter order.
If a factory receives configuration and logger, reversing deps exchanges the arguments and can produce an incorrect result even when parameter names look clear. Resolution does not depend on guessing variable names. Choose the strategy from the actual value origin and creation responsibility.
Aliases preserve an instance
useExisting lets one token alias another existing provider. During a migration, LegacyLogger can resolve the same object supplied for NewLogger. Using useClass: NewLogger in both providers can create two instances with separate buffers or counters.
Copying properties does not restore identity or guarantee future updates. For an extension with multiple handlers, multi: true expresses a different contract: contributions are collected in an array for the token. Do not confuse this aggregation with an alias or a policy returning only the last handler.
The hierarchy defines sharing
In normal resolution, Angular first searches the element’s ElementInjector and walks element ancestors. If no provider is found, it searches the EnvironmentInjector hierarchy from the appropriate origin. The first applicable match determines the value; local providers can override a provider available in root.
In the example, each DraftEditor owns a DraftState instance. The child DraftPreview finds its editor’s provider because it creates no other provider for that token. The other editor is a sibling, not an ancestor.
Sharing between them would require moving responsibility to an appropriate common scope.
Modify lookup deliberately
inject options change a specific resolution contract. self restricts lookup to the current injector instead of walking ancestors. optional permits null when no result exists, avoiding a missing-dependency error. With self and optional, a provider only in the parent is not suddenly found: the missing local result remains null.
This distinction is useful for an optional capability that should exist only when declared on the element itself. Before using an option to hide an error, check whether the design intended a required dependency, inheritance or isolation.
Injection context is synchronous
inject can be used in documented contexts such as field initializers and construction of a DI-created class, or in a provider factory. An ordinary method later called by an event gains no context merely by belonging to a component. runInInjectionContext runs a function with an injector, but that context applies to the synchronous stack frame.
An asynchronous callback or code after await does not inherit permission. Resolve the dependency beforehand and retain its reference when appropriate, or explicitly enter a valid context at the resolution point.
Review identity and lifetime together
The example uses private writable state and a read-only view to concentrate changes in the service. That choice would not suffice with incorrect provider scope: two editors could still share the same root instance. Review should trace the token from consumer to winning provider, confirm creation or aliasing and identify the scope owner.
When a component with a local provider is destroyed, its associated instance participates in that lifecycle. External resources started by the service still need an explicit cleanup policy.
// Reading example: Angular 22; application bootstrap is outside this excerpt.
import {Component, Injectable, inject, signal} from '@angular/core';
@Injectable()
export class DraftState {
private readonly value = signal('');
readonly text = this.value.asReadonly();
setText(text: string) { this.value.set(text); }
}
@Component({
selector: 'draft-preview', standalone: true,
template: `<p>Preview: {{ state.text() }}</p>`,
})
export class DraftPreview { readonly state = inject(DraftState); }
@Component({
selector: 'draft-editor', standalone: true, imports: [DraftPreview],
providers: [DraftState],
template: `<label>Draft <input #entry [value]="state.text()"
(input)="state.setText(entry.value)"></label><draft-preview />`,
})
export class DraftEditor { readonly state = inject(DraftState); }
@Component({
selector: 'draft-workspace', standalone: true, imports: [DraftEditor],
template: `<draft-editor /><draft-editor />`,
})
export class DraftWorkspace {}Each DraftEditor declares its own provider. Its DraftPreview declares no other provider and therefore finds the editor’s instance in the hierarchy. Typing in the first editor updates its preview without changing the second.
State is local and represents no remote save; destroying the editor ends that instance’s scope.
Common pitfalls
Creating two tokens with the same description; using interfaces as runtime tokens; replacing useExisting with useClass; storing per-editor state in a root service; calling inject after await; interpreting optional as automatic lookup in another scope.
Related topics: Components, templates and view identity · Signals, inputs and output contracts · Lifecycle, effects and reactive cleanup
Follow four questions: what is the token, how is its value created, where does lookup begin and who ends its lifetime? An explicit answer to each distinguishes incorrect configuration, unwanted sharing and use outside an injection context.
Reference: Token runtime identity · Angular 22; standalone components