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.

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