Aller au contenu

Composons avec l'injection

La dernière version d'Angular étend l'utilisation de l'injection, facilitant la création de composables mais complexifiant les tests.

Légos -Compasbales -Angular 

Introduction

Avec la dernière version d'Angular, l'injection devient encore plus puissante.

A partir de la version 14, l'injection n'est plus uniquement réservée aux constructeurs de classe, elle pourra s'utiliser hors du contexte d'injection.

Cette nouveauté ouvre bien des possibilités, notamment la possibilité de créer des composables.

Comment cela fonctionne ?
Comment l'intégrer dans nos applications ?

La réponse à ces deux questions se trouve dans la nouvelle implémentation de la fonction inject.

A quoi sert la fonction inject ?

La fonction inject permet d'injecter un "token", et par la même occasion d'en obtenir la référence, dans l'injecteur actif courant.

Dans les précédentes versions d'Angular, cette fonction ne pouvait s'utiliser uniquement dans le contexte de l'injection.

export const MY_HTTP = new InjectionToken('MY_HTTP', {
  provideIn: 'root',
  useFactory() {
    return inject(HttpClient);
  }
})

@Injectable()
export class TodoService {
  http = inject(MY_HTTP);
}

Et maintenant ?

A partir de la version 14 d'Angular, cette fonction inject va pouvoir s'utiliser hors du contexte d'injection à condition que cette dernière soit utilisée au moment:

  • de la construction d'une classe
  • pour initialiser un paramètre de cette même classe.

Pour faire simple, cette fonction va pouvoir s'utiliser dans les composants, directives et pipes.

@Component({
  selector: 'app-todo',
  templateUrl: './todo.component.html',
  styleUrls: './todo.component.less'
})
export class TodoComponent {
  todos$ = inject(TodoService).getTodos();
}

La composition

Avec cette nouvelle manière de faire, écrire des fonctions à fortes réutilisabilités devient très facile.

Pour les développeurs Vue, cette nouveauté peut s'apparenter à la composition.

export function getParam<T>(key: string): Observable<T> {
  const route = inject(ActivatedRoute);
  return route.paramMap.pipe(
    map(params => params.get(key)),
    distinctUntilChanged()
  );
}
@Component({
  selector: 'app-todo-details',
  templateUrl: './todo-details.component.html',
  styleUrls: './todo-details.component.less'
})
export class TodoComponent {
  todoId$ = getParam<Todo>('id');
  todo$ = todoId$.pipe(
    switchMap(id => inject(TodoService).getTodo(id))
  );
}

Un autre cas d'utilisation très pratique est la destruction des observables quand le composant est détruit.

Pour réaliser notre logique de destruction, Angular nous propose le hook OnDestroy présent dans le ViewRef.

Il devient dès lors très facile d'écrire une logique générique de destruction d'observables.

export function destroyed() {
  const replaySubject = new replaySubject(1);

  inject(DestroyRef).onDestroy(() => {
    replaySubject.next(true);
    replaySubject.complete();
  });

  return <T>() => takeUntil<T>(replaySubject.asObservable());
}
@Component({
  selector: 'app-todo-details',
  templateUrl: './todo-details.component.html',
  styleUrls: './todo-details.component.less'
})
export class TodoComponent {
  unsubscribe$ = destroyed();
  refreshDetails$ = new Subject<void>();
  ngOnInit(): void {
    this.refreshDetails$.pipe(unsubscribe$).subscribe();
  }
}

Conclusion

Cette nouvelle fonctionnalité est sans nul doute très puissante et les possibilités d'utilisations et de compositions sont quasiment sans limites mais elle possède aussi des inconvénients.

Premièrement, cette nouveauté ne peut s'utiliser uniquement lors de la construction de composants. Cela a pour impact que l'utilisation des paramètres d'entrées Input d'un composant sont impossibles. Toutefois un workaround est possible en utilisant les closures, mais je le déconseille fortement.

Deuxièmement, les composants vont être plus compliqués à tester. En effet les mocks vont être difficiles à écrire.

Dernier