Go back

Pourquoi Angular est un mauvais framework



Introduction

Le titre est probablement provocateur mais je le crois assez juste. Pour le justifier, je vais lister tous les points qui, selon moi, font d'Angular un mauvais framework.

Note : je comparerai Angular à React et Svelte. React en raison de sa popularité et Svelte en raison de relatives similitudes que certains ont pu voir avec Angular. Mais contrairement à Angular, je considère Svelte comme un bon framework car…

Angular ne permet pas de voir le code inutilisé

Buzz l'éclair disant à Woody : « Du code mort… du code mort partout. »

Comme dans Angular les fichiers TypeScript et HTML sont séparés, vous ne pouvez pas savoir si une méthode est utilisée ou non. Dans l'exemple ci-dessous, la méthode decrement est inutilisée mais votre IDE ne vous en informera pas. Imaginez faire du réusinage de code

avec des dizaines de méthodes !

Une capture de code montrant qu'Angular ne met pas en évidence les méthodes non utilisées.

À l'inverse, sur React ou Svelte, comme la vue et le TypeScript sont dans le même fichier, vous serez informé immédiatement des problèmes (que cela soit pour une fonction inutilisée ou l'appel à une fonction qui n'existe pas).

Une capture de code montrant que Svelte et React mettent en évidence les méthodes non utilisées.
ICON
Objection

N'est-il pas possible de détecter les méthodes non utilisées avec le linter ?

Non, le linter d'Angular ne permet pas de détecter les méthodes inutilisées. Par ailleurs la configuration de base d'Angular est très insuffisante, cela sera mon prochain point.

Angular n'offre aucun cadre

J'entends répéter qu'Angular offre un cadre, une architecture comparé aux autres frameworks. Où voyez-vous cela ?

Est-ce qu'Angular inclue dans son linter une configuration pour imposer un style de codage (comme airbnb) ? Non.

Est-ce que linter est complet ? Même pas. Vous devez ajouter une version spécifique d'ESLint vous-même.

Est-ce qu'Angular inclue un formateur de code pour un style homogène ? Non. Et comme la syntaxe du HTML n’est pas conventionnelle sur Angular vous devez fouiller pour trouver comment mettre en place Prettier convenablement.

Est-ce qu'Angular offre une architecture ? Non. C'est une prétendue qualité que l'on répète sans réfléchir mais Angular en réalité ne vous impose aucune architecture (vous pouvez ou non utiliser des modules (je reviendrai sur ce point), utiliser le nom que vous souhaitez pour vos fichiers, les placer où vous le souhaitez et la prolifération d'utilitaires fait qu'on ne sait jamais quelle est la manière officielle de procéder pour réaliser une tâche).

À l'inverse, un framework comme NextJS impose véritablement une architecture : vous êtes obligés de nommer vos fichiers d'une certaine manière (par exemple vous ne pouvez pas nommer une page autrement que page.js) et le système de routing impose de suivre rigoureusement une certaine structure de dossiers.

ICON
Objection

Mais Angular est plus modulaire que les autres frameworks !

Nommer des fichiers « Modules » ne suffit pas pour qualifier un framework de modulaire. De la même manière, avoir un logo en forme de bouclier ne vous garantie aucune sécurité supplémentaire, je vous assure.

Une lutte sémantique et stylistique constante

Avec Angular, vous aurez constamment des problèmes liés à la sémantique de votre HTML et d'autres à la gestion de vos styles CSS. Avant d'y venir, voyons comment les composants Svelte et React sont affichés dans le DOM. Commençons par Svelte :

<script>
import Component from "./Component.svelte";
</script>
<main>
<Component />
</main>

Passons maintenant à React :

import Component from "./Component";
export default function App() {
return (
<main>
<Component />
</main>
)
}

Comme vous le voyez, le résultat est assez prévisible. Qu'en est-il d'Angular ?

import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<main>
<app-component></app-component>
</main>
`,
})
export class AppComponent {}

Surprise ! Au lieu d'afficher le composant directement, Angular enveloppe le tout d'une balise supplémentaire. C'est un point problématique qui peut vite mener à des contortions aussi pénibles qu'inutiles. En effet, si vous voulez par exemple créer des composants pour une table personnalisée, vous vous retrouverez avec une sémantique incorrecte :

<main>
<app-table _nghost-ng-c4098229708=""
><table _ngcontent-ng-c4098229708="">
<app-table-head
_ngcontent-ng-c4098229708=""
_nghost-ng-c167012404=""
><thead _ngcontent-ng-c167012404="">
<tr _ngcontent-ng-c167012404="">
<th _ngcontent-ng-c167012404="" scope="col">
Employee Name
</th>
<th _ngcontent-ng-c167012404="" scope="col">
Department
</th>
<th _ngcontent-ng-c167012404="" scope="col">Project</th>
<th _ngcontent-ng-c167012404="" scope="col">
Hours/Week
</th>
<!--bindings={
"ng-reflect-ng-for-of": "[object Object],[object Object"
}-->
</tr>
</thead></app-table-head
><app-table-body
_ngcontent-ng-c4098229708=""
_nghost-ng-c775862262=""
><tbody _ngcontent-ng-c775862262="">
<app-table-row
_ngcontent-ng-c775862262=""
app-table-row=""
ng-reflect-employee="[object Object]"
><td>Employee 1</td>
<td>Engineering</td>
<td>Website Redesign</td>
<td>21</td></app-table-row
><app-table-row
_ngcontent-ng-c775862262=""
app-table-row=""
ng-reflect-employee="[object Object]"
><td>Employee 2</td>
<td>Design</td>
<td>Q4 Sales</td>
<td>30</td></app-table-row
><app-table-row
_ngcontent-ng-c775862262=""
app-table-row=""
ng-reflect-employee="[object Object]"
><td>Employee 3</td>
<td>Engineering</td>
<td>Mobile App</td>
<td>37</td></app-table-row
><!--bindings={
"ng-reflect-ng-for-of": "[object Object],[object Object"
}-->
</tbody></app-table-body
><app-table-footer
_ngcontent-ng-c4098229708=""
_nghost-ng-c2143765403=""
><tfoot _ngcontent-ng-c2143765403="">
<tr _ngcontent-ng-c2143765403="">
<th
_ngcontent-ng-c2143765403=""
scope="row"
colspan="3"
>
Total Weekly Hours
</th>
<td _ngcontent-ng-c2143765403="">75</td>
</tr>
</tfoot></app-table-footer
>
</table></app-table
>
</main>

On se moque parfois de Tailwind en raison de la surcharge qu'il impose aux classes HTML, mais Angular est bien pire dans le domaine, comme vous pouvez le voir ! De plus, la table ne s'affichera pas comme une table HTML classique car toute la sémantique est brisée. La même table, réalisée avec React serait rendue de cette manière :

<main>
<table>
<thead>
<tr>
<th scope="col">Employee Name</th>
<th scope="col">Department</th>
<th scope="col">Project</th>
<th scope="col">Hours/Week</th>
</tr>
</thead>
<tbody>
<tr>
<td>Employee 1</td>
<td>Engineering</td>
<td>Website Redesign</td>
<td>21</td>
</tr>
<tr>
<td>Employee 2</td>
<td>Design</td>
<td>Q4 Sales</td>
<td>30</td>
</tr>
<tr>
<td>Employee 3</td>
<td>Engineering</td>
<td>Mobile App</td>
<td>37</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row" colspan="3">Total Weekly Hours</th>
<td>75</td>
</tr>
</tfoot>
</table>
</main>
ICON
Objection

Il existe au moins trois techniques pour contourner ce problème !

Pour commencer, ce problème n'existe pas sur les autres frameworks. Ensuite, aucune des solutions proposées n'est pertinente. Utiliser des ng-container est impossible car on ne peut pas y passer d'attributs. Utiliser ViewContainerRef est d'une complexité absurde et ce n'est pas la façon normale de créer des composants avec Angular. En ce qui concerne les attribute-selectors, ils ne font que renforcer le deuxième problème souligné en introduction et que je vais à présent développer.

Entourer ainsi les composants de balises ou utiliser les attribute-selectors pose des problèmes au niveau de la stylisation des composants qui obligent à briser le principe de responsabilité unique. Reprenons une version minimaliste de notre table (juste une ligne et une cellule) :

import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<tr>
<app-table-cell></app-table-cell>
</tr>
`,
})
export class AppComponent {}

Si l'on utilise à présent les attribute-selectors pour rétablir une bonne sémantique, nous nous retrouvons dans cette situation :

import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<tr>
<td class="cell" app-table-cell></td>
</tr>
`,
styles: ['.cell { background: red }'],
})
export class AppComponent {}

Le problème est que l'on est obligé de déporter tout le style, qui devrait être encapsulé dans le composant enfant (Cell component) dans le composant parent ! Voilà pourquoi j'affirme qu'Angular brise principe de responsabilité unique et que c'est un mauvais framework.

Angular ne possède aucun gestionnaire de mise en cache des données

Il y a probablement deux choses qu'il ne faut pas coder soi-même dans une application web : l'authentification et la mise en cache des données serveur. Ces deux points sont absolument essentiels et je suis étonné de voir qu'Angular ne propose aucun outil pour gérer la mise en cache des données !

Heureusement, presque tous les autres frameworks peuvent compter sur TanStack Query pour gérer ce besoin.

ICON
Objection

— Je viens du futur et TanStack Query est enfin utilisable avec Angular !
— Heureux de l'apprendre. L'équipe de développement d'Angular n'y est probablement pour rien.

L'écosystème est pauvre

En guise d'écho avec le point précédent, il est manifeste que l'écosystème d'Angular est assez pauvre.

Prenons par exemple la principale bibliothèque de composants d'Angular : Angular Material. Elle comprend 36 composants (avec beaucoup de lacunes comme l'absence de masques) et son équivalent en React (MUI) en comprend plus de 50 avec en plus des utilitaires et la possibilité de l'utiliser en mode headless (la qualité de la documentation est aussi sensiblement supérieure). Il n'existe d'ailleurs à ce jour aucune bibliothèque de type Headless sur Angular.

Comparé à Angular, vous trouverez presque toujours une réponse à votre question si vous êtes avec React et vous pourrez utiliser ce framework même pour des usages exotiques comme la création de courriels, la création d'applications en ligne de commande, la création d'expériences en 3D, l'édition de vidéos et même faire des applications en réalité virtuelle !

Enfin, en terme de popularité, les résultats du JavaScript Rising Stars de 2023 montrent qu'Angular est sur la peinte descendante par rapport aux autres frameworks front-end. Angular a certes un passé très honorable, mais il n'a à mes yeux aucun avenir.

Impossibilité d'utiliser des littéraux de gabarits dans le HTML

L'absence de cette syntaxe dans le HTML peut mener à des situations inutilement complexes avec Angular.

Sur la plupart des frameworks, vous pouvez les utiliser sans aucun problème :

Exemple d'usage de littéraux de gabarits dans le HTML avec Svelte et React.

Mais pas avec Angular :

Exemple d'usage de littéraux de gabarits dans le HTML avec Angular. Tout le code du HTML est souligné d'un rouge vif.

Note : l'équipe d'Angular est au courant.

Complexité inutile

Un bon framework permet de faire beaucoup en écrivant peu. Angular permet de faire peu en écrivant beaucoup, notamment à cause de sa complexité parfaitement inutile. Voici quelques points spécifiques.

Les animations

Pour se rendre compte de la complexité d'Angular à l'égard des animations, nous allons essayer de reproduire cet effet avec Svelte, React, puis Angular:

  • Item 1
  • Item 2
  • Item 3

C'est un exemple assez simple qui consiste à agrandir un élément de liste au survol et le faire rebondir au clic.

Version React

Pour réaliser des animations avec React, nous pouvons compter sur Framer Motion, un bibliothèque d'animation simple d'utilisation et permettant de créer des animations complexes.

Commençons par poser la structure de la page :

import classes from './App.module.css';
export default function App() {
const items = [
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' },
];
return (
<ul>
{items.map((item) => (
<li key={item.id} className={classes.box}>
{item.text}
</li>
))}
</ul>
);
}

Pour animer les éléments de liste, il nous faut remplacer nos li par des motion.li afin d'étendre l'élément HTML avec l'API de Framer Motion :

import { motion } from 'framer-motion';
import classes from './App.module.css';
export default function App() {
const items = [
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' },
];
return (
<ul>
{items.map((item) => (
<motion.li key={item.id} className={classes.box}>
{item.text}
</motion.li>
))}
</ul>
);
}

Il ne reste plus qu'à ajouter trois lignes pour indiquer l'effet de la transition (j'ai opté pour une animation de type spring), ainsi que les événements concernés (survol et clic) :

import { motion } from 'framer-motion';
import classes from './App.module.css';
export default function App() {
const items = [
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' },
];
return (
<ul>
{items.map((item) => (
<motion.li
key={item.id}
className={classes.box}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
transition={{ type: 'spring', stiffness: 400, damping: 10 }}
>
{item.text}
</motion.li>
))}
</ul>
);
}

Il nous a donc suffit d'ajouter trois lignes de code pour réaliser l'effet souhaité ! C'est ce que j'apprécie particulièrement avec cette bibliothèque : on peut faire des effets complexes avec un nombre de lignes de code minimal.

Version Svelte

Pour être parfaitement honnête, je n'ai pas réussi à créer l'effet présenté aussi simplement que je l'aurais voulu car je me suis retrouvé dans la situation où tous les éléments de liste grandissaient en même temps.

Cela dit, Svelte possède de nombreux utilitaires très pratiques pour créer des animations, notamment les animations de type FLIP, des transitions intégrées et également l'effet spring que l'on peut créer ainsi pour un effet au survol :

<script>
import { spring } from "svelte/motion";
let scale = spring(1, {
stiffness: 0.05,
damping: 0.1,
});
</script>
<div
class="animated-box"
on:mouseenter={() => scale.set(1.2)}
on:mouseleave={() => scale.set(1)}
style="transform: scale({$scale})"
>
Hover me!
</div>
<style>
/* ... */
</style>

Il existe également un équivalent à Framer Motion pour Svelte et pour ceux qui veulent des composants animés, Svelte Animation Components présente un florilège assez impressionnant.

Version Angular

Enfin, voyons comment les choses se passent sur Angular. Voici le code permettant d'avoir la structure globale de la page :

import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
items = [
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' },
];
}

Premier piège : il vous faut importer le BrowserAnimationsModule au bon endroit sinon vos animations ne fonctionneront pas.

Une fois cela fait… retroussez vos manches ! Nous allons devoir importer animate, keyframes, state, style transition et trigger depuis @angular/animations.

Contrairement à Framer Motion ou Svelte, Angular ne met à disposition aucun utilitaire pour faire des animations de type Spring ou FLIP donc nous allons devoir coder un effet approximatif à la main. La définition de l'animation se fait dans le composant TypeScript de cette manière :

import {
animate,
keyframes,
state,
style,
transition,
trigger,
} from '@angular/animations';
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
animations: [
trigger('listItem', [
state(
'default',
style({
transform: 'scale(1)',
})
),
state(
'active',
style({
transform: 'scale(1.03)',
})
),
transition('default => active', [
animate(
'450ms cubic-bezier(0.4, 0, 0.2, 1)',
keyframes([
style({ transform: 'scale(1)', offset: 0 }),
style({ transform: 'scale(1.04)', offset: 0.4 }),
style({ transform: 'scale(1.01)', offset: 0.7 }),
style({ transform: 'scale(1.03)', offset: 1 }),
])
),
]),
transition('active => default', [
animate(
'500ms cubic-bezier(0.4, 0, 0.2, 1)',
keyframes([
style({ transform: 'scale(1.03)', offset: 0 }),
style({ transform: 'scale(0.98)', offset: 0.4 }),
style({ transform: 'scale(1.01)', offset: 0.7 }),
style({ transform: 'scale(1)', offset: 1 }),
])
),
]),
]),
],
})
export class AppComponent {
items = [
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' },
];
}

Ensuite, il vous faut créer vous-même des méthodes et une variable pour suivre l'état de l'animation :

import {
animate,
keyframes,
state,
style,
transition,
trigger,
} from '@angular/animations';
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
animations: [
// Couvrez ce code que je ne saurais voir !
],
})
export class AppComponent {
items = [
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' },
];
listItemAnimationState: 'default' | 'active' = 'default';
onListItemMouseEnter() {
this.listItemAnimationState = 'active';
}
onListItemMouseLeave() {
this.listItemAnimationState = 'default';
}
}

Voilà pour ce qui est du TypeScript (ou pas). Il nous maintenant relier le tout au HTML :

<ul>
<li
class="box"
*ngFor="let item of items"
[@listItem]="listItemAnimationState"
(mouseenter)="onListItemMouseEnter()"
(mouseleave)="onListItemMouseLeave()"
>
{{ item.text }}
</li>
</ul>

Verdict : cela ne fonctionne toujours pas. Lorsqu'on passe le curseur sur un élément de liste, tous les éléments grandissent en même temps !

  • Item 1
  • Item 2
  • Item 3

Il faut donc coder à la main un système afin d'assigner le bon état à chaque élément de liste… C'est d'une complexité positivement ridicule. Voici le code final, d'un total de 86 lignes pour obtenir ce que React fait en 26 lignes, et en mieux (d'ailleurs je n'ai même pas ajouté l'effet au clic sur Angular, cela aurait encore complexifié ce composant qui est déjà en surcharge pondérale).

import {
animate,
keyframes,
state,
style,
transition,
trigger,
} from '@angular/animations';
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
animations: [
trigger('listItem', [
state(
'default',
style({
transform: 'scale(1)',
})
),
state(
'active',
style({
transform: 'scale(1.03)',
})
),
transition('default => active', [
animate(
'450ms cubic-bezier(0.4, 0, 0.2, 1)',
keyframes([
style({ transform: 'scale(1)', offset: 0 }),
style({ transform: 'scale(1.04)', offset: 0.4 }),
style({ transform: 'scale(1.01)', offset: 0.7 }),
style({ transform: 'scale(1.03)', offset: 1 }),
])
),
]),
transition('active => default', [
animate(
'500ms cubic-bezier(0.4, 0, 0.2, 1)',
keyframes([
style({ transform: 'scale(1.03)', offset: 0 }),
style({ transform: 'scale(0.98)', offset: 0.4 }),
style({ transform: 'scale(1.01)', offset: 0.7 }),
style({ transform: 'scale(1)', offset: 1 }),
])
),
]),
]),
],
})
export class AppComponent implements OnInit {
items = [
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' },
];
animationStates: { [key: number]: 'default' | 'active' } = {};
ngOnInit(): void {
for (let index in this.items) {
this.animationStates[index] = 'default';
}
}
onListItemMouseEnter(index: number) {
this.animationStates[index] = 'active';
}
onListItemMouseLeave(index: number) {
this.animationStates[index] = 'default';
}
}

En résumé, Angular ne possède aucune classe utilitaire pour gérer les animations comme sur Svelte, il n'existe actuellement pas de bibliothèques d'animation (car comme je l'ai dit, l'écosystème est pauvre) et le code pour arriver à un résultat potable est d'une complexité inutile.

RxJS

RxJs est une bibliothèque qui nécessite d'être utilisée pour tous les traitements asynchrones sur Angular. Cela ajoute une complixité incroyable à tous les niveaux. Voyons comment cela se matérialise dans deux exemples : l'appel à une API et la réalisation d'une Todo-list.

Appel API

Même si Svelte et React peuvent compter sur Tanstack Query pour faciliter les appels API, je vais travailler sans bibliothèque par souci d'équité. Voici un exemple d'appel API avec Svelte et et React :

<script>
let promise = fetch("https://jsonplaceholder.typicode.com/todos").then(
(response) => response.json()
);
</script>
{#await promise}
<p>Loading...</p>
{:then data}
<pre>
{JSON.stringify(data, null, 2)}
</pre>
{:catch error}
<p>Could not fetch data.</p>
{/await}

Les deux exemples se laissent comprendre assez facilement, pour le cas de Svelte, le seul élément spécifique à connaître concerne les blocs await...then.
Pour React il faut connaître deux choses : une méthode de cycle de vie (useEffect) ainsi que le concept de state qui est central. Tout le reste est du JavaScript classique. Voyons à présent ce qu'il faut comprendre du côté d'Angular.

import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { catchError, throwError } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AppService {
constructor(private http: HttpClient) { }
private handleError(error: HttpErrorResponse) {
if (error.status === 0) {
// A client-side or network error occurred. Handle it accordingly.
console.error('An error occurred:', error.error);
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong.
console.error(
`Backend returned code ${error.status}, body was: `, error.error);
}
// Return an observable with a user-facing error message.
return throwError(() => new Error('Something bad happened; please try again later.'));
}
fetchData() {
return this.http.get('https://jsonplaceholder.typicode.com/todos').pipe(
catchError(this.handleError)
)
}
}

Je ne vais pas détailler toutes les étapes, mais pour un simple appel API, il vous faut comprendre le concept général d'injection de service, d'observable, de souscription, la gestion des erreurs avec catchError, la méthode de cycle de vie OnInit et la directive structurelle NgIf.

À ceci s'ajoute la difficulté d'utiliser ces éléments de façon correcte. En effet, il existe plusieurs façons de faire un appel API et de souscrire à une réponse, notamment en utilisant le pipe async (encore un concept en plus pour surcharger notre mémoire). Cet article montre rapidement à quoi ressemblerait l'implémentation d'un appel API avec cette méthode. Osez dire qu'Angular offre un cadre après ça !

Todo-list

Pour cet exemple, je ferai une version simplifiée d'une Todo-list en ne permettant que l'ajout d'un Todo (cela sera suffisant pour illustrer mon propos). Voici ce que nous allons créer (sans le bouton de nettoyage) :

  • Rant about Angular.
  • Break a chair.

Commençons par une implémentation avec Svelte et React :

<script lang="ts">
import type { Todo } from "./types/Todo";
let newTodoText = "";
let todos: Todo[] = [
{ id: 1, text: "Rant about Angular." },
{ id: 2, text: "Break a chair." },
];
function handleSubmit(event: SubmitEvent) {
event.preventDefault();
if (!newTodoText.trim()) return;
const newTodo = {
id: Date.now(),
text: newTodoText.trim(),
};
todos = [...todos, newTodo];
newTodoText = "";
}
</script>
<form on:submit={handleSubmit}>
<label for="add-todo">Your new todo</label>
<input id="add-todo" type="text" bind:value={newTodoText} />
<button type="submit">Add</button>
</form>
<ul>
{#each todos as todo (todo.id)}
<li>{todo.text}</li>
{/each}
</ul>

Les deux versions sont assez similaires et l'ensemble ne s'éloigne pas beaucoup du JavaScript traditionnel. Le seul concept supplémentaire à assimiler serait celui de props (on le retrouve sur Svelte et React) car dans un cas réel, on aurait créé un composant séparé pour le formulaire et un autre pour la liste de Todos (et probablement un autre composant pour le Todo).

Voyons à quoi ressemble ce même code avec Angular :

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
import { Todo } from './models/Todo';
@Injectable({
providedIn: 'root',
})
export class TodoListService {
todosChanged = new Subject<Todo[]>();
private todos: Todo[] = [
{
id: 1,
text: 'Rant about Angular.',
},
{
id: 2,
text: 'Break a chair.',
},
];
getTodos() {
return this.todos.slice();
}
addTodo(todoItem: Todo) {
this.todos.push(todoItem);
this.todosChanged.next(this.todos.slice());
}
}

Encore une fois, nous nous traînons avec de la complexité inutile, notamment avec les Subjects (qui se décomposent en BehaviorSubject, ReplaySubject, AsyncSubject et Void subject. Ne me demandez pas l'utilité). De plus, le simple fait d’interagir avec un formulaire nécessite de patauger dans une complexité propre à Angular (dans cet exemple je suis allé au plus simple, mais dans un exemple réel vous devrez utiliser les reactive forms avec toute la complexité qu'ils embarquent).

Les modules

Difficile de faire un exemple comparatif car les modules sont un élément propre à Angular et qui n'ajoutent aucune fonctionnalité. J'ai comme l'impression qu'ils n'ont été ajoutés que pour pouvoir s'attribuer l'étiquette de "framework modulaire". Si vous avez eu l'occasion de travailler avec ces derniers, vous devez probablement avec quelques séquelles.

Fait amusant : l'équipe d'Angular semble pousser les développeurs à utiliser les standalone components pour réduire le besoin d'utiliser les modules. Peut-être est-ce un aveu indirect de l'inutilité de cette chose.

Écrire des composants est pénible

L'intérêt principal d'utiliser un framework est de pouvoir diviser son code en composants réutilisables. Généralement, cela est assez simple, il suffit de créer un nouveau fichier (avec un clic droit à l'emplacement désiré ou avec un raccourcis clavier) et de coder.

Avec Angular, la complexité d'un composant fait qu'il est nécessaire d'utiliser l'outil en ligne de commande pour générer vos composants. Le problème est que c'est une tache fastidieuse :

  • Cela nécessite d'ouvrir un terminal.
  • Vous devez écrire à la main le chemin complet pour créer votre composant
  • Si vous voulez créer un composant se trouvant par exemple dans : shared/ui/tables et que vous vous trompez (en écrivant par exemple « table » au lieu de « tables  »), vous devez non seulement supprimer les fichiers créés, mais en plus supprimer sa déclaration dans le fichier app.module.ts
ICON
Objection

N'est-il pas possible de créer les fichiers à la main comme sur les autres frameworks dans ce cas ?

C'est une possibilité, mais cela demande une quantité de travail ridicule :

  • La simple création de la classe est très complexe (nécessite d'utiliser le décorateur, de l'importer, et de se souvenir de la syntaxe à utiliser). D'ailleurs la syntaxe change si votre classe est un composant, service, module ou directive. Inutile de gaspiller sa mémoire avec ça.
  • Vous devez créer un dossier et trois fichiers à la main. C'est laborieux.
  • Vous devez ensuite déclarer votre fichier dans le bon module (ce n'est pas toujours évident).
Quatre fichiers ouverts simultanément dans l'IDE avec un total de 33 lignes pour créer un composant Angular.

À titre de comparaison, avec React ou Svelte vous n'avez rien de tout cela à penser. Un composant React est juste une fonction (que vous pouvez écrire en deux frappes avec un snippet) et un composant Svelte peut tenir en une ligne.

Exemples de composants React et Svelte. Le composant Svelte comporte une seule ligne et le composant React en comporte trois.

Multiplication et séparation des fichiers

Cette section fait écho ce qui précède. Un des points qui cause le plus de fatigue lorsqu'on code avec Angular est la multiplication et séparation des fichiers.

Lorsque je parle de séparation des fichiers, je fais allusion à l'architecture MVC qui se reflète dans l'organisation des fichiers sur Angular (HTML, TS et Services). Cette approche possède à mes yeux trois problèmes :

  1. La relation entre les éléments n'est plus évidente. En séparant la vue et le controller, il est difficile de voir précisément quel est l'effet de l'interaction de l'utilisateur sur l'interface. Ce problème est d'ailleurs renforcé par la liaison de données qui est bidirectionnelle sur Angular. À l'inverse, sur React les données ne vont que dans un seul sens (ce qui rend l'architecture et la recherche de la source de données beaucoup plus facile).
  2. Cette une conséquence du point précédent : comme les éléments ont une liaison faible entre eux, il est très facile de se retrouver dans une situation où l'on ne sait plus quel est l'utilité d'une méthode ou si un style CSS est utilisé ou non. On se retrouve alors avec un résidu constant de code mort ou à l'utilité incertaine (cf Angular ne permet pas de voir le code inutilisé).
  3. Jongler de fichiers en fichiers est épuisant. Lorsque j'utilise Svelte ou React, je peux me concentrer sur une fonctionnalité, généralement contenue dans un composant. Sur Angular je dans sans cesse passer d'un fichier à l'autre pour mettre en place une fonctionnalité.

Tout est lent

Toutes les opérations sur Angular semblent prendre du temps. Le simple fait de lancer le serveur de développement peut prendre plusieurs minutes sur un projet avancé et le hot-reload répond souvent avec plusieurs secondes de retard.

À titre d'exemple, voici un projet « hello world » lancé simultanément avec React et Angular :

Résultat : le projet Angular met environ dix fois plus de temps à charger. Le temps d'installation des dépendances est également plus long.

La génération des listes est bancale

C'est pourtant un élément fondamental et Angular ne semble pas disposer des outils basiques pour générer une liste efficacement. Si vous utilisez Svelte ou React, vous savez certainement qu'il est recommandé (même obligatoire dans le cas de React) d'ajouter une key à chaque élément de liste pour éviter les rendus inutiles et se prémunir de certains comportements inattendus lors de la manipulation des listes :

Exemple d'utilisation de la props 'key' avec React et Svelte.

Je pensais au début qu'Angular n'avait pas besoin de key (vu que contrairement à React, aucune erreur ne s'affiche si l'on ne la fournit pas) mais en fouillant dans les méandres de la documentation on apprend qu'il est nécessaire d'écrire une fonction trackBy pour éviter les problèmes liés à la gestion des listes.

Imaginez à présent que vous êtes en train de développer une application qui génère des dizaines de listes différentes. Écrire une fonction trackBy pour chaque composant est plus que laborieux. Il faudrait donc, juste pour avoir les fonctionnalités de base de tout framework front, créer tout un gestionnaire dédié à la gestion des listes. Je vous laisse lire cet article Medium qui montre toute la complexité de la chose.

Fait amusant : vous pouvez écrire n'importe quoi dans votre fonction trackBy, Angular ne générera aucune erreur.

Utilisation d'une fonction affichant un console.log dans la balise 'trackBy' d'Angular. Ancune erreur n'est détectée dans l'IDE.

L'implémentation d'i18n est laborieuse

Pour mieux s'en rendre compte, voyons ensemble comment mettre en place i18n sur Angular.

Une fois la configuration initiale faite, il vous faut ajouter des attributs i18n dans votre template pour indiquer les éléments à traduire:

<element i18n="{i18n_metadata}">{string_to_translate}</element>

Une fois cela fait, il faut ensuite lancer une commande pour générer le fichier de traduction de référence (qui servira de modèle pour les autres langues):

Terminal window
ng extract-i18n --output-path src/locale

À présent, comment générer les fichiers de traduction pour les autres langues ? Avec un copié-collé !

Terminal window
cp src/locales/messages.xlf src/locales.fr.xlf

Un problème évident se pose alors : que faire lorsqu'on veut mettre à jour le fichier de traduction ? De laborieuses acrobaties à base de copié-collé. Imaginez ce que cela représente comme travail lorsqu'on travaille avec plusieurs langues.
Cela dit, il existe une bibliothèque qui résout ce problème. Pourquoi devoir passer par une bibliothèque pour quelque chose comme ça ?

À ceci s'ajoutent d'autres limitations :

  • La traduction est dépendante du routeur. Dans la plupart des autres frameworks, il est possible de changer la langue de façon dynamique sans changer l'URL de la page. Avec l'implémentation de base d'i18n sur Angular, cela n'est pas possible.
  • Il n'y a aucun support TypeScript pour aider à la traduction (l'implémentation de base d'i18n sur Angular n'utilise pas de clés de traduction d'ailleurs). C'est pourtant quelque chose qu'on retrouve sur d'autres frameworks comme Vue ou React. Pour ce qui est de Svelte, un comportement similaire peut être atteint avec typesafe-i18n mais cette bibliothèque n'est pas propre à Svelte et n'est plus maintenue.

Gestions des inputs

Sur la plupart des frameworks, vous pouvez ajouter des attributs à vos composants afin de les rendre génériques et réutilisables. De plus, si vous oubliez un attribut, votre IDE vous en informera et vous pouvez également rendre certains attributs optionnels :

Exemple de composants React et Svelte avec des attributs. Si un attribut obligatoire est manquant, une erreur est affichée.

Prenons le même exemple avec Angular à présent :

Exemple de composants Angular avec des attributs. Aucune erreur n'est affichée si un attribut semblant obligatoire est omis.

Comme vous le remarquez, aucune erreur n'est détectée et il est impossible de savoir quels sont les attributs nécessaires ou non à moins de passer au peigne fin le composant.

ICON
Objection

— Depuis la version 16 d'Angular il est possible d'ajouter des required inputs.
— Cela veut dire que les développeurs ont eu à gérer les composants ainsi pendant sept années… Mais inutile d'être trop enthousiaste, l'API des Inputs reste trop complexe et tous les Input sont facultatifs par défaut alors qu'ils devraient être obligatoires.

Pollution du DOM

Au cours du développement, vous verrez votre DOM pollué par des commentaires HTML ajoutés par Angular. Cela rend la lecture et le débogage pénible :

Une représentation du DOM avec plusieurs commentaires HTML parasités ajoutés par Angular.

Pollution de l'environnement

Ne pas utiliser Angular a même un effet bénéfique sur l'environnement ! Outre ces chiffres qui sont à prendre cum grano salis, le bundle size d'Angular reste beaucoup plus important que les autres frameworks, ce qui peut avoir un effet sur les performances et les coûts d'infrastructure.

…et d'autres choses que je ne prendrai pas le temps de détailler

Même à voix basse.