
Angular Inject & Injection Functions - Patterns & Anti-Patterns

š§ The State Management Use Case
Grouping with Modules
UnreadEmailsBadgeComponent
EmailListComponent
@NgModule({
declarations: [UnreadEmailsBadgeComponent, EmailListComponent],
exports: [UnreadEmailsBadgeComponent, EmailListComponent],
imports: [EffectsModule.forFeature(emailEffects)]
})
class EmailModule {}
Facade Module
@Injectable()
class EmailFacade {
unreadEmails$ = this._store.select(selectUnreadEmails());
constructor(private _store: Store) {}
...
}
@NgModule({
imports: [EffectsModule.forFeature(emailEffects)],
providers: [EmailFacade]
})
class EmailModule {}
The Problem Without Modules
emailsEffects
@Component({
providers: [provideEffects(emailEffects)]
})
class UnreadEmailsBadgeComponent {}
Alex's PR
inject()
inject()
14.0.0-rc.1
inject() + InjectionToken solution
inject()
const EmailStore = new InjectionToken<Store>('EmailStore', {
factory() {
const store = inject(Store);
store.registerEffects(emailEffects);
return store;
}
});
@Component(...)
class UnreadEmailsBadgeComponent {
/* `store` type is `Store` thanks to type inference
* and `InjectionToken` being a generic. */
store = inject(EmailStore);
}
@Inject()
constructor(@Inject(EmailStore) store: Store)
š The Boilerplate Reduction Use Case
Injection Functions
inject()
@Component(...)
class RecipesComponent {
addRecipe = injectAction(addRecipe);
recipes$ = injectSelection(selectRecipes);
}
function injectAction(action) {
return (...) => inject(Store).dispatch(action);
}
function injectSelection(selector) {
return inject(Store).select(selector);
}
šŖ The Type Inference Use Case
@Inject()
constructor(@Inject(HttpClient) http: number) {}
Alternative
inject()
const MyToken = new InjectionToken<number>('MyToken');
@Component(...)
class MyCmp {
value = inject(MyToken); // value type is number
}
šŗ The Abstract Base Class Use Case
inject()
@Directive()
abstract class Base {
a = inject(A);
}
@Component()
class Cmp extends Base {
constructor(b: B) {
super();
}
}
inject()
ā³ Anti-Pattern #1: Hacking Declarable's Provider
RxState
ComponentStore
ngOnDestroy
@Component({
providers: [RxState]
})
class MyCmp {
constructor(private _state: RxState) {}
}
@Self()
function injectState(initialState) {
const config = inject(RxStateConfig);
const state = new RxState(config);
state.set(initialState);
return state;
}
@Component(...)
class MyCmp {
state = injectState(initialState);
}
Detecting onDestroy
Lifecycle Hook
onDestroy
function injectState() {
/* @hack wrongly assuming that ChangeDetectorRef is a ViewRef
* Yeah! CDR is actually the ViewRef of the component. */
const viewRef = inject(ChangeDetectorRef) as ViewRef;
/* Initialize state. */
const state = new RxState();
state.set(initialState);
/* Unsubscribe from everything on destroy.
* @hack queue microtask otherwise this breaks in devMode due
* to the following condition: https://github.com/angular/angular/blob/0ff4eda3d4bd52fb145285a472e9c0237ea8e68f/packages/core/src/render3/instructions/shared.ts#L804-L806
* Credits to Chau Tran for raising the issue.*/
queueMicrotask(() => viewRef.onDestroy(() => state.ngOnDestroy()));
}
Hooking Into Change Detection
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<div>{{ state.counter }}</div>
<div>
<button (click)="state.counter = 0">RESET</button>
<button (click)="increment()">+</button>
</div>`,
})
export class CounterComponent {
state = injectState({
counter: 0,
});
increment() {
/* Adding a dirty setTimeout to show that reactivity works anyway. */
setTimeout(() => {
this.state.counter += 1;
}, 500);
}
}
Scoped Providers
@Component({
providers: [
{
provide: State,
useClass: State,
scope: 'self'
}
]
}
class MyCmp {
}
š Anti-Pattern #2: Hacking Dependency Injection
Configurable Dependency
@Injectable()
class Paginator {
constructor(@Self() config: PaginatorConfig, http: HttpClient) {}
}
@Injectable()
abstract class PaginatorConfig {
limit: number;
}
@Component({
providers: [
Paginator,
{
provide: PaginatorConfig,
useValue: {
limit: 10
} as PaginatorConfig
}
]
})
class MyCmp {
constructor(private _paginator: Paginator) {
}
}
The Factory
@Component(...)
class MyCmp {
paginator = this._factory.create({limit: 10});
constructor(private _factory: PaginatorFactory) {}
}
@Injectable({providedIn: 'root'})
class PaginatorFactory {
constructor(private _http: HttpClient) {}
create(config: PaginatorConfig) {
return new Paginator(config, {http: this._http});
}
}
Factory Injection Function
@Component(...)
class MyCmp {
paginator = injectPaginator({limit: 10});
}
function injectPaginator(config) {
return new Paginator(config);
}
class Paginator {
http = inject(HttpClient);
constructor(config) {}
}
šŖ Anti-Pattern #3: Hacking Lifecycle Hooks
ngOnDestroy
@Component(...)
class MyCmp {
hold = injectHolder();
ngOnInit() {
/* This will automatically unsubscribe on */
this.hold(interval(1000), value => console.log(value));
}
}
function injectHolder() {
const subscription = new Subscription();
const viewRef = inject(ChangeDetectorRef) as ViewRef;
queueMicrotask(() => viewRef.onDestroy(subscription.unsubscribe()));
return (source$, observer) => {
const sub = source$.subscribe(observer);
subscription.add(sub);
return sub;
}
}
š
°ļø Alex Rickabaugh's Recommendations
š Additional Clarifications & Recommendations
TestBed.configureTestingModule({providers: [MyCmp, ...testDoubles]});
const cmp = TestBed.inject(MyCmp);