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

A few days ago, at an absurd and not very known time of day where it's nighttime on all timezones around the globe, (Angular Team), (NgRx Team), and I started a long discussion about the impact of 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 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:

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.

So what alternatives do we have?

Grouping with Modules

Well, one of the main benefits of using Angular modules is to .

For instance, if I am providing a feature or a library where UnreadEmailsBadgeComponent and EmailListComponent can't work without 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 , , and me, you are a big fan of you can use a facade that loads the effects when the module is loaded.

@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 somehow 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 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 šŸŽ‰

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 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).

šŸŽ’ 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 .

. In fact, as opposed to traditional injection, Injection Functions can have .

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

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

const MyToken = new InjectionToken<number>('MyToken');

@Component(...)
class MyCmp {
  value = inject(MyToken); // value type is number
}

@Directive()
abstract class Base {
  a = inject(A);
}

@Component()
class Cmp extends Base {
  constructor(b: B) {
    super();
  }
}

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

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

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

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

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

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

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

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

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

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

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

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

TestBed.configureTestingModule({providers: [MyCmp, ...testDoubles]});
const cmp = TestBed.inject(MyCmp);