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 .

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

@Injectable()
class EmailFacade {
  unreadEmails$ = this._store.select(selectUnreadEmails());
  constructor(private _store: Store) {}
  ...
}

@NgModule({
  imports: [EffectsModule.forFeature(emailEffects)],
  providers: [EmailFacade]
})
class EmailModule {}

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

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

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