Marmicode
Blog Post
Younes Jaaidi

Angular Inject & Injection Functions - Patterns & Anti-Patterns

by Younes Jaaidi ā€¢ 
May 20, 2022 ā€¢ 12 minutes
Angular Inject & Injection Functions - Patterns & Anti-Patterns

Last Update: 2022-06-02

A few days ago, at an absurd and not very known time of day where it's nighttime on all timezones around the globe, Alex Rickabaugh (Angular Team), Brandon Roberts (NgRx Team), and I started a long discussion about the impact of standalones on different use cases like lazy-loaded services that are not provided in root.

šŸ§  The State Management Use Case

Here's an example. Some state management libraries like NgRx need to load "Effects" in order to fulfill the indirection they are based on. This is usually done by adding one of the following imports in Angular modules:

  • EffectsModule.forRoot(myEffects)
  • EffectsModule.forFeature(myEffects)

The best place to import these effects is at the highest shared point of the components that depend on them but where is it? šŸ¤” Importing all effects at the root level will clutter the main bundle. šŸ¤Æ Importing the effects at the route level can be sufficient but what about the day we decide to reuse a component under that route in another route. Not only the effects will be missing, but even worse, things get flaky and the component in the second route will only work if we navigate to the first route where the effects are loaded.

So what alternatives do we have?

Grouping with Modules

Well, one of the main benefits of using Angular modules is to group highly coupled things.

For instance, if I am providing a feature or a library where UnreadEmailsBadgeComponent and EmailListComponent can't work without emailEffects then I can just group everything in a module. This way, to use one of the components, the feature/library users will have to import the module, thus implicitly loading the effects and we don't let much space for mistakes.

@NgModule({ declarations: [UnreadEmailsBadgeComponent, EmailListComponent], exports: [UnreadEmailsBadgeComponent, EmailListComponent], imports: [EffectsModule.forFeature(emailEffects)] }) class EmailModule {}

Facade Module

Otherwise, if as Lars, Alex, and me, you are a big fan of SCAM you can use a facade that loads the effects when the module is loaded.

note that the facade is not provided in root because we want to make sure that its users will import the EmailModule with the effects before using the facade

@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

Now, suppose we are implementing a full-standalone app, how can we handle this without modules? We have to think about new patterns.

We could provide the effects in the components that depend on emailsEffects:

@Component({ providers: [provideEffects(emailEffects)] }) class UnreadEmailsBadgeComponent {}

where provideEffects somehow (I am saying "somehow" because this is clearly not as straightforward as we might think. Trust me!) registers the effects if they are not already registered, but this is error-prone and can quickly be omitted.

Alex's PR

During the long discussion with Alex & Brandon, we shared all kinds of different ideas until we reached the point where Alex told us that we could almost use the inject() function in the constructor of the component. It didn't work due to a little tree-shakability optimization and Alex seemed to be having that in mind for a while. The next thing I know is that the day after, Alex fixed the issue with this PR https://github.com/angular/angular/pull/45991 šŸŽ‰

The inject() is now usable in the constructor since 14.0.0-rc.1.

inject() + InjectionToken solution

So, how can inject() solve this?

The initial idea was to provide an injection token alias with a side effect that would work like this:

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); }

While this is still possible with @Inject() decorator, this solution is less error-prone and needs less boilerplate than this: constructor(@Inject(EmailStore) store: Store).

āš ļø WARNING: inject() can only be used in construction context (i.e. in the call stack of constructor() and of course fields initialization).

šŸŽ’ The Boilerplate Reduction Use Case

Injection Functions

Sometimes, we just want to travel light and reduce the boilerplate. šŸ’” As we can use inject() in the construction context, we could also wrap it in a function. I like to call these "Injection Functions".

An Angular Injection Function is a synchronous function that directly or indirectly injects services using the inject() function. Angular Injection Functions can only be used in the construction context of a declarable (e.g. component, directive, pipe) or a service.

The main benefit of Injection Functions is their composability. In fact, as opposed to traditional injection, Injection Functions can have parameters.

@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); }

By following the principle of least surprise, I'd recommend prefixing Injection Function with inject. This will also help implement lint rules to detect Injection Function misuse.

šŸ’Ŗ The Type Inference Use Case

As mentioned before, when using injection tokens or even classes with the @Inject() decorator, it's not very explicit but not only do we have to guess the type of the injected value but we are also casting it to it and we can do something wrong like this without any compilation error.

constructor(@Inject(HttpClient) http: number) {}

Alternative

We can avoid this using the inject() function as it will infer the type properly from the InjectionToken.

const MyToken = new InjectionToken<number>('MyToken'); @Component(...) class MyCmp { value = inject(MyToken); // value type is number }

šŸ”ŗ The Abstract Base Class Use Case

While we will generally prefer composition to inheritance, there are mixin-like use cases, where, I must admit, inheritance can come in handy and reduce boilerplate even though it comes with some trouble which is now solved thanks to the availability context of inject() function.

A component can now extend a class that injects services without having to pass all parameters to the parent constructor if the component needs to implement a constructor for injecting services itself or for any other reason.

@Directive() abstract class Base { a = inject(A); } @Component() class Cmp extends Base { constructor(b: B) { super(); } }

Chau Tran, with whom we shared a couple of frustrations and excitements about inject() šŸ¤ÆšŸ„³, summarizes it very well here https://nartc.me/blog/inheritance-angular-inject

ā³ Anti-Pattern #1: Hacking Declarable's Provider

A few years ago, my friend Michael Hladky started some great work on ephemeral states. This inspired both RxAngular's State and NgRx's ComponentStore.

The main challenge with both implementations is that we need a new instance of a local state for each component while still preventing child components from accessing the parent's state.

We could instantiate RxState or ComponentStore manually but they wouldn't be able to hook into ngOnDestroy. Also, they won't be able to have access to dependency injection if they need it someday (e.g. grabbing global configuration, initial state šŸ˜œšŸ¤«).

That's why we need to use dependency injection:

@Component({
  providers: [RxState]
})
class MyCmp {
  constructor(private _state: RxState) {}
}

The main problem with this approach is that if we forget the provider, we might end up with the parent's state. We could use @Self() decorator to avoid this but if we can think about this then we can also think about the provider. I can't think about any one of them šŸ˜…. In the same way, the child components have access to the instance and we can't prevent that... except by using an Injection Function and it could look something like this:

function injectState(initialState) {
  const config = inject(RxStateConfig);
  const state = new RxState(config);
  state.set(initialState);
  return state;
}

@Component(...)
class MyCmp {
  state = injectState(initialState);
}

šŸŽ Bonus: we could get type inference from the initial state.

Detecting onDestroy Lifecycle Hook

But wait! Local state has to detect when the component is destroyed. Sadly, AFAIK, there is no clean way of doing so but we can hack our way through with some ugly down-casting šŸ„“ like this:

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())); }

2022-06-02 UPDATE - āš ļø WARNING: Downcasting ChangeDetectorRef to ViewRef is a very problematic hack and might break at any time, even in a future Angular minor version. This should never be used in production. Also, scheduling the onDestroy in a microtask is very fragile. In fact, not only it's quite unpleasant to read, but there might be some extreme cases where the callback might not be called.

Hooking Into Change Detection

The next intuitive step is hooking into Change Detection and triggering it when necessary. Wouldn't this be cool? A progressive approach for learning Angular without scarificing performance? Note the ChangeDetectionStrategy.OnPush.

@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); } }

Well, you know what! This works and you can play with it right now on stackblitz: https://stackblitz.com/edit/inject-state?file=src%2Fapp%2Fapp.component.ts

āš ļø WARNING: While it is fun to experiment with this kind of hacks and observe their impact on DX, performance etc... they should be avoided for a couple of reasons that I regrouped at the end of this article.

Scoped Providers

šŸ¤” Actually, we wouldn't need any of these hacks if we could simply declare a provider as provided in "self" or something like that. This would mean that child components can't inject it.

@Component({
  providers: [
    {
      provide: State,
      useClass: State,
      scope: 'self'
    }
  ]
}
class MyCmp {
}

šŸ­ Anti-Pattern #2: Hacking Dependency Injection

Sometimes we might need a factory to create configurable instances of a service that has dependencies.

Configurable Dependency

Before Injection Functions, we were able to do so like this:

@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) {
  }
}

šŸ˜“ Tedious! Right? It's not only relatively complex but we can't instantiate multiple instances in the component and we can't prevent children from grabbing our instance.

The Factory

As this doesn't solve our problem, the other technique is using a factory like this:

@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}); } }

This is way better with the factory as children can't access the service and the bonus is that we can even have multiple instances in the same component but it is still quite verbose and the manual dependency injection is an additional thing to maintain.

Factory Injection Function

Finally, using an Injection Function, we can substantially simplify this:

@Component(...)
class MyCmp {
  paginator = injectPaginator({limit: 10});
}

function injectPaginator(config) {
  return new Paginator(config);
}

class Paginator {
  http = inject(HttpClient);
  constructor(config) {}
}

2022-06-02 UPDATE - āš ļø WARNING: While this less verbose approach is quite attractive, it is still quite hacky. First, the HttpClient is not very explicit, and one might be surprised when trying to instantiate Paginator manually. Also, instantiating the Paginator manually is kind of defeating the purpopse of Angular dependency injection. Finally, we don't have any proper approach to hook into ngOnDestroy lifecycle hook.

šŸŖ„ Anti-Pattern #3: Hacking Lifecycle Hooks

2022-06-02 UPDATE: If we can hook into ngOnDestroy lifecycle hook, then why not just implement a subscription holder that keeps your subscriptions alive until the component is destoryed... šŸ¤”

@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;
  }
}

2022-06-02 UPDATE - āš ļø WARNING: Well, just like the two previous anti-patterns, less boilerplate is always exciting but at what cost? In fact, this can break in some extreme cases or simply in a future major or even minor version of Angular.

šŸ…°ļø Alex Rickabaugh's Recommendations

  • āœ… Stick with APIs that require the user to add something to providers in order to be able to inject it.
  • āœ… Contain side effects in the factories of those providers.
  • āœ… Use inject as a convenience in constructors to avoid the awkwardness of @Inject when dealing with InjectionToken.
  • āœ… Don't try to build "magical" Injection Functions that secretly create things outside the DI system.

šŸ“ Additional Clarifications & Recommendations

  • āœ… inject() and Injection Functions are not meant to replace traditional DI. They are only here to solve the issues described above.
  • āœ… inject() and Injection Functions are not the building blocks for React-like hooks. They could allow something similar to Vue's Composition API but this is not the goal here.
  • āœ… Injection Functions are very contextual. They simply don't work outside of the factory or constructor call stack. That's the reason why we need a very explicit naming convention where we can easily tell that this only works in this context because it's using inject() underneath. I'd recommend using the inject prefix. This way it should be easy to implement a fast and reliable lint rule ensuring that inject() and Injection Functions are used in the right place.
  • āœ… Using traditional Dependency Injection or inject() or Injection Functions is an implementation detail. Switching from one to another shouldn't affect tests. This is the reason why tests, especially "isolated tests" should not be instantiating components manually (e.g. new MyCmp(httpClient, state)) and should be using the TestBed. In addition to this, we don't want our tests to get coupled to the order of the injected services and we might not want to provide test doubles for all dependencies.
TestBed.configureTestingModule({providers: [MyCmp, ...testDoubles]}); const cmp = TestBed.inject(MyCmp);

šŸ‘ Special thanks to Alex & Brandon for the long discussions we shared and also for reviewing this post.

šŸ‘‹ Happy Ng14!

šŸ’» injectState demo: https://stackblitz.com/edit/inject-state

šŸ’¬ Let's discuss this on github: https://github.com/marmicode/marmicode/discussions/4.