Angular / Intermediate / Lesson 7
Dependency injection and service scope
Choose where a dependency is provided, understand the lifetime of the resulting instance, and call inject only within a supported runtime context.
On this page
Lesson goals
- Distinguish a token, provider, injector, and instance.
- Explain root and component-scoped service state.
- Test dependency replacement without global mutable configuration.
A token identifies a dependency, not a global variable
Dependency injection separates an operation from the concrete objects it needs. A provider tells an injector how to supply a value for a token, using a value, class, factory, or another token. InjectionToken is useful for configuration and interface-shaped contracts because TypeScript interfaces do not exist as runtime tokens. The injector owns resolution according to its hierarchy and scope. Two identical-looking token descriptions do not make two token objects the same identity. Keep each exported token defined once and import that same object wherever it is provided or requested.
Verify hierarchy with a complete local injector
The complete program supplies one token in a root injector and overrides it in a child. The child reads its local value while the root keeps its own. This exercises actual Angular injector resolution without a component, application bootstrap, or browser. The explicit destroy calls end these local injector lifetimes. A component-scoped provider follows the same ownership question but also participates in Angular’s view hierarchy, so it needs component tests. Do not interpret providedIn: root as one process-wide singleton across every independently bootstrapped application or server request. Scope depends on the owning injector.
import { InjectionToken, Injector } from "@angular/core";
const LABEL = new InjectionToken<string>("course label");
const root = Injector.create({ providers: [{ provide: LABEL, useValue: "root" }] });
const child = Injector.create({ parent: root, providers: [{ provide: LABEL, useValue: "feature" }] });
console.log(root.get(LABEL), child.get(LABEL));
child.destroy();
root.destroy();
root feature
The line is root feature. The override changes child resolution without modifying the root provider.
Decide who owns mutable service state
The contextual service contains selection state and is provided at a component boundary. Each owning component instance receives its own service instance, while descendants can resolve that instance unless a nearer provider overrides it. Providing the same state service again in a child can unexpectedly split state that was meant to be shared. A root-provided service instead shares state within its application injector lifetime. Choose scope from the feature’s ownership and cleanup requirements, not from a reflex that every service should be global. The fragment requires Angular creation of its host; directly constructing the class does not reproduce that scope.
import { Component, Injectable, inject, signal } from "@angular/core";
@Injectable()
export class SelectionStore { readonly selected = signal<number | null>(null); }
@Component({
selector: "app-selection-panel", standalone: true, providers: [SelectionStore],
template: `<p>Selected: {{ store.selected() ?? "none" }}</p>`
})
export class SelectionPanel { readonly store = inject(SelectionStore); }
Two independently created panel hosts would own separate selections. This ownership observation needs an Angular component fixture and is not executed here.
Context is a runtime condition, not a class name
inject is valid in supported injection contexts, including field initialization or construction performed by Angular DI and provider factories. A normal event handler or ngOnInit call is not automatically such a context. Capture the dependency during supported initialization and use the captured value later. runInInjectionContext can establish a synchronous context for a given injector, but it does not keep that context active across await. Passing an Injector explicitly is another supported option for APIs that accept it. Missing providers and calls outside context are different failures and should not be repaired by adding unrelated global providers.
Test identity, replacement, and cleanup
Test whether two consumers should receive the same instance, whether a child override is intentional, and whether destroying the owner releases its resources. Replace external dependencies with narrow test providers rather than editing production configuration. A unit test calling a pure service method can verify domain logic, while a DI test verifies construction and scope. Avoid service constructors that perform network requests or other hidden work before the test can configure a fake. Log provider failures with useful token context without exposing secrets. An understandable hierarchy lets state sharing be reviewed explicitly instead of discovered through accidental singleton behavior.
Common errors and fixes
inject throws outside a supported context.
- Why it happens
- It is called from an ordinary later method.
- How to fix it
- Capture the dependency during supported initialization.
- Prevent it next time
- Keep context-sensitive API calls near their owner creation.
Two child views unexpectedly have different shared state.
- Why it happens
- A nearer provider creates another service instance.
- How to fix it
- Provide at the intended common owner and remove accidental overrides.
- Prevent it next time
- Test service identity across the hierarchy.
A token cannot resolve despite matching descriptions.
- Why it happens
- Different InjectionToken objects were created.
- How to fix it
- Import the single exported token object.
- Prevent it next time
- Treat token object identity as part of the contract.
Check your understanding
1. Remove the child override in the local injector program. Predict the line and explain whether the root value was copied or resolved through the parent.
Show worked solution
The line becomes root root because the child can resolve the token through its parent. This is hierarchical lookup, not a copied provider configuration or a new independent token. Add a local resolution test and, for mutable service instances, an identity assertion so intended sharing is protected. Component hierarchy behavior still needs its own fixture.
2. A component button handler calls inject(SelectionStore). Describe the safe refactor without changing service scope.
Show worked solution
Initialize a field with inject(SelectionStore) while Angular constructs the component, then have the handler use that field. Keep the provider at the same intended owner. Moving the service to root merely to hide the context error changes state lifetime and can create a new bug. Test the event path and verify the service instance remains the intended one.
Key takeaways
- Provider location determines ownership and scope.
- Token identity and injection context are runtime facts.
- Test DI behavior separately from pure service logic.