Angular vs. React : lequel est le meilleur pour le développement Web ?
Publié: 2022-03-11Il existe d'innombrables articles sur la question de savoir si React ou Angular est le meilleur choix pour le développement Web. Avons-nous encore besoin d'un autre?
La raison pour laquelle j'ai écrit cet article est qu'aucun des articles déjà publiés - bien qu'ils contiennent de grandes idées - n'est suffisamment approfondi pour qu'un développeur front-end pratique puisse décider lequel peut répondre à ses besoins.
Dans cet article, vous apprendrez comment Angular et React visent tous deux à résoudre des problèmes frontaux similaires, mais avec des philosophies très différentes, et si choisir l'un ou l'autre est simplement une question de préférence personnelle. Pour les comparer, nous allons construire deux fois la même application, une fois avec Angular puis une autre fois avec React.
Annonce prématurée d'Angular
Il y a deux ans, j'ai écrit un article sur l'écosystème React. Entre autres points, l'article affirmait qu'Angular avait été victime de "mort par annonce préalable". À l'époque, le choix entre Angular et presque tout le reste était facile pour quiconque ne voulait pas que son projet s'exécute sur un framework obsolète. Angular 1 était obsolète, et Angular 2 n'était même pas disponible en version alpha.
Avec le recul, les craintes étaient plus ou moins justifiées. Angular 2 a radicalement changé et a même subi une réécriture majeure juste avant la version finale.
Deux ans plus tard, nous avons Angular 4 avec une promesse de stabilité relative à partir de maintenant.
Maintenant quoi?
Angular vs. React : comparer les pommes et les oranges
Certaines personnes disent que comparer React et Angular revient à comparer des pommes à des oranges. Alors que l'un est une bibliothèque qui traite des vues, l'autre est un cadre à part entière.
Bien sûr, la plupart des développeurs React ajouteront quelques bibliothèques à React pour en faire un framework complet. Là encore, le flux de travail résultant de cette pile est souvent encore très différent d'Angular, de sorte que la comparabilité est encore limitée.
La plus grande différence réside dans la gestion de l'état. Angular est livré avec une liaison de données intégrée, alors que React est aujourd'hui généralement complété par Redux pour fournir un flux de données unidirectionnel et travailler avec des données immuables. Ce sont des approches opposées à part entière, et d'innombrables discussions sont en cours pour savoir si la liaison mutable/données est meilleure ou pire que immuable/unidirectionnelle.
Une zone de niveau de jeu
Comme React est notoirement plus facile à pirater, j'ai décidé, aux fins de cette comparaison, de créer une configuration React qui reflète Angular assez étroitement pour permettre une comparaison côte à côte des extraits de code.
Certaines fonctionnalités angulaires qui se démarquent mais ne sont pas dans React par défaut sont :
Caractéristique | Forfait angulaire | Bibliothèque de réaction |
---|---|---|
Liaison de données, injection de dépendance (DI) | @ angulaire / noyau | MobX |
Propriétés calculées | rxjs | MobX |
Routage basé sur les composants | @angular/routeur | Réagir Routeur v4 |
Composants de conception matérielle | @angulaire/matériel | Boîte à outils de réaction |
CSS étendu aux composants | @ angulaire / noyau | Modules CSS |
Validation des formulaires | @angular/forms | ÉtatFormulaire |
Générateur de projet | @angular/cli | Scripts de réaction TS |
Liaison de données
La liaison de données est sans doute plus facile à démarrer que l'approche unidirectionnelle. Bien sûr, il serait possible d'aller dans le sens complètement opposé, et d'utiliser Redux ou mobx-state-tree avec React, et ngrx avec Angular. Mais ce serait un sujet pour un autre post.
Propriétés calculées
En ce qui concerne les performances, les getters simples dans Angular sont tout simplement hors de question car ils sont appelés à chaque rendu. Il est possible d'utiliser BehaviorSubject de RsJS, qui fait le travail.
Avec React, il est possible d'utiliser @computed de MobX, qui atteint le même objectif, avec sans doute une API un peu plus agréable.
Injection de dépendance
L'injection de dépendance est un peu controversée car elle va à l'encontre du paradigme React actuel de programmation fonctionnelle et d'immuabilité. Il s'avère qu'une sorte d'injection de dépendance est presque indispensable dans les environnements de liaison de données, car elle aide au découplage (et donc à la simulation et aux tests) lorsqu'il n'y a pas d'architecture de couche de données distincte.
Un autre avantage de DI (pris en charge dans Angular) est la possibilité d'avoir différents cycles de vie de différents magasins. La plupart des paradigmes React actuels utilisent une sorte d'état global de l'application qui correspond à différents composants, mais d'après mon expérience, il est trop facile d'introduire des bogues lors du nettoyage de l'état global lors du démontage du composant.
Avoir un magasin créé sur le montage du composant (et être disponible de manière transparente pour les enfants de ce composant) semble être un concept vraiment utile et souvent négligé.
Prêt à l'emploi dans Angular, mais assez facilement reproductible avec MobX également.
Routage
Le routage basé sur les composants permet aux composants de gérer leurs propres sous-routes au lieu d'avoir une grande configuration de routeur global. Cette approche a finalement été adoptée par react-router
dans la version 4.
Conception matérielle
Il est toujours agréable de commencer avec des composants de niveau supérieur, et la conception matérielle est devenue quelque chose comme un choix par défaut universellement accepté, même dans les projets non Google.
J'ai délibérément choisi React Toolbox plutôt que l'interface utilisateur matérielle généralement recommandée, car Material UI a de sérieux problèmes de performances avoués avec son approche CSS en ligne, qu'ils prévoient de résoudre dans la prochaine version.
De plus, PostCSS/cssnext utilisé dans React Toolbox commence de toute façon à remplacer Sass/LESS.
CSS délimité
Les classes CSS sont quelque chose comme des variables globales. Il existe de nombreuses approches pour organiser le CSS afin d'éviter les conflits (y compris BEM), mais il existe une tendance actuelle claire à utiliser des bibliothèques qui aident à traiter le CSS pour éviter ces conflits sans qu'un développeur frontal n'ait besoin de concevoir des systèmes de nommage CSS élaborés.
Validation du formulaire
Les validations de formulaires sont une fonctionnalité non triviale et très largement utilisée. C'est bien d'avoir ceux couverts par une bibliothèque pour éviter la répétition du code et les bogues.
Générateur de projet
Avoir un générateur CLI pour un projet est juste un peu plus pratique que de devoir cloner des passe-partout à partir de GitHub.
Même application, construite deux fois
Nous allons donc créer la même application en React et Angular. Rien de spectaculaire, juste un Shoutboard qui permet à n'importe qui de poster des messages sur une page commune.
Vous pouvez essayer les applications ici :
- Shoutboard angulaire
- Shoutboard Réagir
Si vous voulez avoir le code source complet, vous pouvez l'obtenir sur GitHub :
- Shoutboard Source angulaire
- Shoutboard Réagir source
Vous remarquerez que nous avons également utilisé TypeScript pour l'application React. Les avantages de la vérification de type dans TypeScript sont évidents. Et maintenant, comme une meilleure gestion des importations, async/wait et rest spread est enfin arrivée dans TypeScript 2, cela laisse Babel/ES7/Flow dans la poussière.
Aussi, ajoutons Apollo Client aux deux parce que nous voulons utiliser GraphQL. Je veux dire, REST est génial, mais après une dizaine d'années, il vieillit.
Amorçage et routage
Examinons d'abord les points d'entrée des deux applications.
Angulaire
const appRoutes: Routes = [ { path: 'home', component: HomeComponent }, { path: 'posts', component: PostsComponent }, { path: 'form', component: FormComponent }, { path: '', redirectTo: '/home', pathMatch: 'full' } ] @NgModule({ declarations: [ AppComponent, PostsComponent, HomeComponent, FormComponent, ], imports: [ BrowserModule, RouterModule.forRoot(appRoutes), ApolloModule.forRoot(provideClient), FormsModule, ReactiveFormsModule, HttpModule, BrowserAnimationsModule, MdInputModule, MdSelectModule, MdButtonModule, MdCardModule, MdIconModule ], providers: [ AppService ], bootstrap: [AppComponent] })
@Injectable() export class AppService { username = 'Mr. User' }
Fondamentalement, tous les composants que nous voulons utiliser dans l'application doivent aller dans des déclarations. Toutes les bibliothèques tierces aux importations et tous les magasins mondiaux aux fournisseurs. Les composants enfants ont accès à tout cela, avec la possibilité d'ajouter plus de choses locales.
Réagir
const appStore = AppStore.getInstance() const routerStore = RouterStore.getInstance() const rootStores = { appStore, routerStore } ReactDOM.render( <Provider {...rootStores} > <Router history={routerStore.history} > <App> <Switch> <Route exact path='/home' component={Home as any} /> <Route exact path='/posts' component={Posts as any} /> <Route exact path='/form' component={Form as any} /> <Redirect from='/' to='/home' /> </Switch> </App> </Router> </Provider >, document.getElementById('root') )
Le composant <Provider/>
est utilisé pour l'injection de dépendances dans MobX. Il enregistre les magasins dans le contexte afin que les composants React puissent les injecter plus tard. Oui, le contexte React peut (sans doute) être utilisé en toute sécurité.
La version React est un peu plus courte car il n'y a pas de déclarations de module - généralement, vous venez d'importer et c'est prêt à l'emploi. Parfois, ce type de dépendance dure est indésirable (tests), donc pour les magasins singleton mondiaux, j'ai dû utiliser ce modèle GoF vieux de plusieurs décennies :
export class AppStore { static instance: AppStore static getInstance() { return AppStore.instance || (AppStore.instance = new AppStore()) } @observable username = 'Mr. User' }
Le routeur d'Angular est injectable, il peut donc être utilisé de n'importe où, pas seulement des composants. Pour obtenir la même chose dans react, nous utilisons le package mobx-react-router et injectons le routerStore
.
Résumé : L'amorçage des deux applications est assez simple. React a l'avantage d'être plus simple, en utilisant uniquement des importations au lieu de modules, mais, comme nous le verrons plus tard, ces modules peuvent être très pratiques. Faire des singletons manuellement est un peu gênant. En ce qui concerne la syntaxe de déclaration de routage, JSON vs JSX n'est qu'une question de préférence.
Liens et navigation impérative
Il y a donc deux cas pour changer de route. Déclaratif, utilisant les éléments <a href...>
, et impératif, appelant directement l'API de routage (et donc de localisation).
Angulaire
<h1> Shoutboard Application </h1> <nav> <a routerLink="/home" routerLinkActive="active">Home</a> <a routerLink="/posts" routerLinkActive="active">Posts</a> </nav> <router-outlet></router-outlet>
Angular Router détecte automatiquement quel routerLink
est actif et y place une classe routerLinkActive
appropriée, afin qu'il puisse être stylé.
Le routeur utilise l'élément spécial <router-outlet>
pour restituer tout ce que le chemin actuel dicte. Il est possible d'avoir de nombreux <router-outlet>
s, car nous approfondissons les sous-composants de l'application.
@Injectable() export class FormService { constructor(private router: Router) { } goBack() { this.router.navigate(['/posts']) } }
Le module de routeur peut être injecté dans n'importe quel service (à moitié magiquement par son type TypeScript), la déclaration private
le stocke alors sur l'instance sans avoir besoin d'une affectation explicite. Utilisez la méthode de navigate
pour changer d'URL.
Réagir
import * as style from './app.css' // … <h1>Shoutboard Application</h1> <div> <NavLink to='/home' activeClassName={style.active}>Home</NavLink> <NavLink to='/posts' activeClassName={style.active}>Posts</NavLink> </div> <div> {this.props.children} </div>
React Router peut également définir la classe du lien actif avec activeClassName
.
Ici, nous ne pouvons pas fournir directement le nom de la classe, car il a été rendu unique par le compilateur de modules CSS, et nous devons utiliser l'assistant de style
. Plus sur cela plus tard.
Comme vu ci-dessus, React Router utilise l'élément <Switch>
dans un élément <App>
. Comme l'élément <Switch>
encapsule et monte simplement la route actuelle, cela signifie que les sous-routes du composant actuel ne sont que this.props.children
. Donc c'est composable aussi.
export class FormStore { routerStore: RouterStore constructor() { this.routerStore = RouterStore.getInstance() } goBack = () => { this.routerStore.history.push('/posts') } }
Le mobx-router-store
permet également une injection et une navigation faciles.
Résumé : Les deux approches du routage sont tout à fait comparables. Angular semble être plus intuitif, tandis que React Router a une composabilité un peu plus simple.
Injection de dépendance
Il s'est déjà avéré avantageux de séparer la couche de données de la couche de présentation. Ce que nous essayons de réaliser avec DI ici est de faire en sorte que les composants des couches de données (appelés ici modèle/magasin/service) suivent le cycle de vie des composants visuels, et permettent ainsi de créer une ou plusieurs instances de ces composants sans avoir besoin de toucher au global Etat. En outre, il devrait être possible de mélanger et d'associer des couches de données et de visualisation compatibles.
Les exemples de cet article sont très simples, donc tous les trucs DI peuvent sembler exagérés, mais ils sont utiles à mesure que l'application se développe.
Angulaire
@Injectable() export class HomeService { message = 'Welcome to home page' counter = 0 increment() { this.counter++ } }
Ainsi, n'importe quelle classe peut être rendue @injectable
, et ses propriétés et méthodes mises à la disposition des composants.
@Component({ selector: 'app-home', templateUrl: './home.component.html', providers: [ HomeService ] }) export class HomeComponent { constructor( public homeService: HomeService, public appService: AppService, ) { } }
En enregistrant le HomeService
auprès des providers
du composant, nous le mettons exclusivement à la disposition de ce composant. Ce n'est plus un singleton maintenant, mais chaque instance du composant recevra une nouvelle copie, fraîche sur le montage du composant. Cela signifie qu'il n'y a pas de données périmées d'une utilisation précédente.
En revanche, AppService
a été enregistré dans app.module
(voir ci-dessus), il s'agit donc d'un singleton et reste le même pour tous les composants, tout au long de la vie de l'application. Pouvoir contrôler le cycle de vie des services à partir des composants est un concept très utile, mais sous-estimé.
DI fonctionne en attribuant les instances de service au constructeur du composant, identifié par les types TypeScript. De plus, les mots-clés public
attribuent automatiquement les paramètres à this
, de sorte que nous n'avons plus besoin d'écrire ces lignes ennuyeuses this.homeService = homeService
.
<div> <h3>Dashboard</h3> <md-input-container> <input mdInput placeholder='Edit your name' [(ngModel)]='appService.username' /> </md-input-container> <br/> <span>Clicks since last visit: {{homeService.counter}}</span> <button (click)='homeService.increment()'>Click!</button> </div>
La syntaxe de modèle d'Angular, sans doute assez élégante. J'aime le raccourci [()]
, qui fonctionne comme une liaison de données bidirectionnelle, mais sous le capot, il s'agit en fait d'une liaison d'attribut + événement. Comme le cycle de vie de nos services l'exige, homeService.counter
va se réinitialiser chaque fois que nous nous éloignons de /home
, mais le appService.username
reste et est accessible de partout.
Réagir
import { observable } from 'mobx' export class HomeStore { @observable counter = 0 increment = () => { this.counter++ } }
Avec MobX, nous devons ajouter le décorateur @observable
à toute propriété que nous voulons rendre observable.
@observer export class Home extends React.Component<any, any> { homeStore: HomeStore componentWillMount() { this.homeStore = new HomeStore() } render() { return <Provider homeStore={this.homeStore}> <HomeComponent /> </Provider> } }
Pour gérer correctement le cycle de vie, nous devons faire un peu plus de travail que dans l'exemple angulaire. Nous encapsulons le HomeComponent
dans un Provider
, qui reçoit une nouvelle instance de HomeStore
sur chaque montage.
interface HomeComponentProps { appStore?: AppStore, homeStore?: HomeStore } @inject('appStore', 'homeStore') @observer export class HomeComponent extends React.Component<HomeComponentProps, any> { render() { const { homeStore, appStore } = this.props return <div> <h3>Dashboard</h3> <Input type='text' label='Edit your name' name='username' value={appStore.username} onChange={appStore.onUsernameChange} /> <span>Clicks since last visit: {homeStore.counter}</span> <button onClick={homeStore.increment}>Click!</button> </div> } }
HomeComponent
utilise le décorateur @observer
pour écouter les changements dans les propriétés @observable
.
Le mécanisme sous le capot est assez intéressant, alors parlons-en brièvement ici. Le décorateur @observable
remplace une propriété dans un objet par un getter et un setter, ce qui lui permet d'intercepter les appels. Lorsque la fonction de rendu d'un composant augmenté @observer
est appelée, ces accesseurs de propriétés sont appelés et ils conservent une référence au composant qui les a appelés.
Ensuite, lorsque setter est appelé et que la valeur est modifiée, les fonctions de rendu des composants qui ont utilisé la propriété lors du dernier rendu sont appelées. Maintenant, les données sur les propriétés utilisées où sont mises à jour, et tout le cycle peut recommencer.
Un mécanisme très simple, et assez performant aussi. Explication plus approfondie ici.
Le décorateur @inject
est utilisé pour injecter des instances appStore
et homeStore
dans les props de HomeComponent
. À ce stade, chacun de ces magasins a un cycle de vie différent. appStore
est le même durant la vie de l'application, mais homeStore
est fraîchement créé à chaque navigation vers la route « /home ».
L'avantage de ceci est qu'il n'est pas nécessaire de nettoyer les propriétés manuellement comme c'est le cas lorsque tous les magasins sont globaux, ce qui est pénible si l'itinéraire est une page de "détail" qui contient des données complètement différentes à chaque fois.
Résumé : En tant que gestion du cycle de vie du fournisseur dans une caractéristique inhérente à la DI d'Angular, il est bien sûr plus simple de l'atteindre là-bas. La version React est également utilisable mais implique beaucoup plus de passe-partout.
Propriétés calculées
Réagir
Commençons par React sur celui-ci, il a une solution plus simple.
import { observable, computed, action } from 'mobx' export class HomeStore { import { observable, computed, action } from 'mobx' export class HomeStore { @observable counter = 0 increment = () => { this.counter++ } @computed get counterMessage() { console.log('recompute counterMessage!') return `${this.counter} ${this.counter === 1 ? 'click' : 'clicks'} since last visit` } }
Nous avons donc une propriété calculée qui se lie à counter
et renvoie un message correctement pluralisé. Le résultat de counterMessage
est mis en cache et recalculé uniquement lorsque le counter
change.
<Input type='text' label='Edit your name' name='username' value={appStore.username} onChange={appStore.onUsernameChange} /> <span>{homeStore.counterMessage}</span> <button onClick={homeStore.increment}>Click!</button>
Ensuite, nous référençons la propriété (et la méthode d' increment
) à partir du modèle JSX. Le champ d'entrée est piloté par la liaison à une valeur et en laissant une méthode de l' appStore
gérer l'événement utilisateur.
Angulaire
Pour obtenir le même effet dans Angular, nous devons être un peu plus inventifs.
import { Injectable } from '@angular/core' import { BehaviorSubject } from 'rxjs/BehaviorSubject' @Injectable() export class HomeService { message = 'Welcome to home page' counterSubject = new BehaviorSubject(0) // Computed property can serve as basis for further computed properties counterMessage = new BehaviorSubject('') constructor() { // Manually subscribe to each subject that couterMessage depends on this.counterSubject.subscribe(this.recomputeCounterMessage) } // Needs to have bound this private recomputeCounterMessage = (x) => { console.log('recompute counterMessage!') this.counterMessage.next(`${x} ${x === 1 ? 'click' : 'clicks'} since last visit`) } increment() { this.counterSubject.next(this.counterSubject.getValue() + 1) } }
Nous devons définir toutes les valeurs qui servent de base à une propriété calculée en tant que BehaviorSubject
. La propriété calculée elle-même est également un BehaviorSubject
, car toute propriété calculée peut servir d'entrée pour une autre propriété calculée.
Bien sûr, RxJS
peut faire bien plus que cela, mais ce serait un sujet pour un article complètement différent. L'inconvénient mineur est que cette utilisation triviale de RxJS pour les propriétés juste calculées est un peu plus détaillée que l'exemple de réaction, et vous devez gérer les abonnements manuellement (comme ici dans le constructeur).
<md-input-container> <input mdInput placeholder='Edit your name' [(ngModel)]='appService.username' /> </md-input-container> <span>{{homeService.counterMessage | async}}</span> <button (click)='homeService.increment()'>Click!</button>
Notez comment nous pouvons référencer le sujet RxJS avec le | async
tuyau | async
. C'est une belle touche, beaucoup plus courte que de devoir s'abonner à vos composants. Le composant input
est piloté par la directive [(ngModel)]
. Malgré son apparence étrange, il est en fait assez élégant. Juste un sucre syntaxique pour la liaison de données de la valeur à appService.username
et l'attribution automatique de la valeur à partir de l'événement d'entrée de l'utilisateur.

Résumé : Les propriétés calculées sont plus faciles à implémenter dans React/MobX que dans Angular/RxJS, mais RxJS pourrait fournir des fonctionnalités FRP plus utiles, qui pourraient être appréciées plus tard.
Modèles et CSS
Pour montrer comment les templates se superposent, utilisons le composant Posts qui affiche une liste de publications.
Angulaire
@Component({ selector: 'app-posts', templateUrl: './posts.component.html', styleUrls: ['./posts.component.css'], providers: [ PostsService ] }) export class PostsComponent implements OnInit { constructor( public postsService: PostsService, public appService: AppService ) { } ngOnInit() { this.postsService.initializePosts() } }
Ce composant relie simplement HTML, CSS et les services injectés et appelle également la fonction pour charger les messages de l'API lors de l'initialisation. AppService
est un singleton défini dans le module d'application, tandis que PostsService
est transitoire, avec une nouvelle instance créée à chaque fois que le composant est créé. Le CSS référencé à partir de ce composant est étendu à ce composant, ce qui signifie que le contenu ne peut rien affecter en dehors du composant.
<a routerLink="/form" class="float-right"> <button md-fab> <md-icon>add</md-icon> </button> </a> <h3>Hello {{appService.username}}</h3> <md-card *ngFor="let post of postsService.posts"> <md-card-title>{{post.title}}</md-card-title> <md-card-subtitle>{{post.name}}</md-card-subtitle> <md-card-content> <p> {{post.message}} </p> </md-card-content> </md-card>
Dans le modèle HTML, nous référençons principalement les composants de Angular Material. Pour les avoir à disposition, il fallait les inclure dans les imports app.module
(voir ci-dessus). La directive *ngFor
est utilisée pour répéter le composant md-card
pour chaque publication.
CSS local :
.mat-card { margin-bottom: 1rem; }
Le CSS local ne fait qu'augmenter l'une des classes présentes sur le composant md-card
.
CSS global :
.float-right { float: right; }
Cette classe est définie dans le fichier global style.css
pour la rendre disponible pour tous les composants. Il peut être référencé de manière standard, class="float-right"
.
CSS compilé :
.float-right { float: right; } .mat-card[_ngcontent-c1] { margin-bottom: 1rem; }
Dans le CSS compilé, nous pouvons voir que le CSS local a été étendu au composant rendu en utilisant le sélecteur d'attribut [_ngcontent-c1]
. Chaque composant angulaire rendu a une classe générée comme celle-ci à des fins de portée CSS.
L'avantage de ce mécanisme est que nous pouvons référencer les classes normalement, et la portée est gérée « sous le capot ».
Réagir
import * as style from './posts.css' import * as appStyle from '../app.css' @observer export class Posts extends React.Component<any, any> { postsStore: PostsStore componentWillMount() { this.postsStore = new PostsStore() this.postsStore.initializePosts() } render() { return <Provider postsStore={this.postsStore}> <PostsComponent /> </Provider> } }
Dans React, encore une fois, nous devons utiliser l'approche du Provider
pour rendre la dépendance PostsStore
"transitoire". Nous importons également des styles CSS, référencés en tant que style
et appStyle
, pour pouvoir utiliser les classes de ces fichiers CSS dans JSX.
interface PostsComponentProps { appStore?: AppStore, postsStore?: PostsStore } @inject('appStore', 'postsStore') @observer export class PostsComponent extends React.Component<PostsComponentProps, any> { render() { const { postsStore, appStore } = this.props return <div> <NavLink to='form'> <Button icon='add' floating accent className={appStyle.floatRight} /> </NavLink> <h3>Hello {appStore.username}</h3> {postsStore.posts.map(post => <Card key={post.id} className={style.messageCard}> <CardTitle title={post.title} subtitle={post.name} /> <CardText>{post.message}</CardText> </Card> )} </div> } }
Naturellement, JSX se sent beaucoup plus JavaScript-y que les modèles HTML d'Angular, ce qui peut être une bonne ou une mauvaise chose selon vos goûts. Au lieu de la directive *ngFor
, nous utilisons la construction map
pour itérer sur les publications.
Maintenant, Angular est peut-être le framework qui vante le plus TypeScript, mais c'est en fait JSX où TypeScript brille vraiment. Avec l'ajout de modules CSS (importés ci-dessus), cela transforme vraiment votre codage de modèle en complétion de code zen. Chaque chose est vérifiée par type. Composants, attributs, voire classes CSS ( appStyle.floatRight
et style.messageCard
, voir ci-dessous). Et bien sûr, la nature allégée de JSX encourage la division en composants et en fragments un peu plus que les modèles d'Angular.
CSS local :
.messageCard { margin-bottom: 1rem; }
CSS global :
.floatRight { float: right; }
CSS compilé :
.floatRight__qItBM { float: right; } .messageCard__1Dt_9 { margin-bottom: 1rem; }
Comme vous pouvez le voir, le chargeur de modules CSS postfixe chaque classe CSS avec un suffixe aléatoire, ce qui garantit l'unicité. Un moyen simple d'éviter les conflits. Les classes sont ensuite référencées via les objets importés du webpack. Un inconvénient possible de cela peut être que vous ne pouvez pas simplement créer un CSS avec une classe et l'augmenter, comme nous l'avons fait dans l'exemple Angular. D'un autre côté, cela peut être une bonne chose, car cela vous oblige à encapsuler correctement les styles.
Résumé : Personnellement, j'aime un peu mieux JSX que les modèles angulaires, en particulier en raison de la prise en charge de la complétion de code et de la vérification de type. C'est vraiment une fonctionnalité qui tue. Angular a maintenant le compilateur AOT, qui peut également repérer quelques éléments, la complétion de code fonctionne également pour environ la moitié des éléments, mais ce n'est pas aussi complet que JSX/TypeScript.
GraphQL - Chargement des données
Nous avons donc décidé d'utiliser GraphQL pour stocker les données de cette application. L'un des moyens les plus simples de créer un back-end GraphQL consiste à utiliser du BaaS, comme Graphcool. C'est donc ce que nous avons fait. Fondamentalement, vous définissez simplement des modèles et des attributs, et votre CRUD est prêt à partir.
Code commun
Comme une partie du code lié à GraphQL est identique à 100 % pour les deux implémentations, ne le répétons pas deux fois :
const PostsQuery = gql` query PostsQuery { allPosts(orderBy: createdAt_DESC, first: 5) { id, name, title, message } } `
GraphQL est un langage de requête visant à fournir un ensemble de fonctionnalités plus riche par rapport aux points de terminaison RESTful classiques. Disséquons cette requête particulière.
-
PostsQuery
est juste un nom pour cette requête à référencer plus tard, il peut être nommé n'importe quoi. -
allPosts
est la partie la plus importante - elle fait référence à la fonction pour interroger tous les enregistrements avec le modèle "Post". Ce nom a été créé par Graphcool. -
orderBy
etfirst
sont des paramètres de la fonctionallPosts
.createdAt
est l'un des attributs du modèlePost
.first: 5
signifie qu'il ne renverra que les 5 premiers résultats de la requête. -
id
,name
,title
etmessage
sont les attributs du modèlePost
que nous voulons inclure dans le résultat. Les autres attributs seront filtrés.
Comme vous pouvez déjà le voir, c'est assez puissant. Consultez cette page pour vous familiariser davantage avec les requêtes GraphQL.
interface Post { id: string name: string title: string message: string } interface PostsQueryResult { allPosts: Array<Post> }
Oui, en bons citoyens TypeScript, nous créons des interfaces pour les résultats GraphQL.
Angulaire
@Injectable() export class PostsService { posts = [] constructor(private apollo: Apollo) { } initializePosts() { this.apollo.query<PostsQueryResult>({ query: PostsQuery, fetchPolicy: 'network-only' }).subscribe(({ data }) => { this.posts = data.allPosts }) } }
La requête GraphQL est une observable RxJS, et nous y souscrivons. Cela fonctionne un peu comme une promesse, mais pas tout à fait, donc nous n'avons pas de chance d'utiliser async/await
. Bien sûr, il reste toPromise, mais cela ne semble pas être la méthode angulaire de toute façon. Nous définissons fetchPolicy: 'network-only'
car dans ce cas, nous ne voulons pas mettre en cache les données, mais les récupérer à chaque fois.
Réagir
export class PostsStore { appStore: AppStore @observable posts: Array<Post> = [] constructor() { this.appStore = AppStore.getInstance() } async initializePosts() { const result = await this.appStore.apolloClient.query<PostsQueryResult>({ query: PostsQuery, fetchPolicy: 'network-only' }) this.posts = result.data.allPosts } }
La version React est presque identique, mais comme l' apolloClient
utilise ici des promesses, nous pouvons profiter de la syntaxe async/await
. Il existe d'autres approches dans React qui "tapent" simplement les requêtes GraphQL sur des composants d'ordre supérieur, mais cela me semblait un peu trop mélanger les données et la couche de présentation.
Résumé : Les idées du RxJS subscribe vs async/wait sont vraiment les mêmes.
GraphQL - Sauvegarde des données
Code commun
Encore une fois, du code lié à GraphQL :
const AddPostMutation = gql` mutation AddPostMutation($name: String!, $title: String!, $message: String!) { createPost( name: $name, title: $title, message: $message ) { id } } `
Le but des mutations est de créer ou de mettre à jour des enregistrements. Il est donc avantageux de déclarer certaines variables avec la mutation car c'est ainsi que l'on y transmet des données. Nous avons donc des variables name
, title
et message
, tapées sous forme de String
, que nous devons remplir chaque fois que nous appelons cette mutation. La fonction createPost
, encore une fois, est définie par Graphcool. Nous spécifions que les clés du modèle Post
auront des valeurs issues de nos variables de mutation, et également que nous voulons que seul l' id
du Post nouvellement créé soit envoyé en retour.
Angulaire
@Injectable() export class FormService { constructor( private apollo: Apollo, private router: Router, private appService: AppService ) { } addPost(value) { this.apollo.mutate({ mutation: AddPostMutation, variables: { name: this.appService.username, title: value.title, message: value.message } }).subscribe(({ data }) => { this.router.navigate(['/posts']) }, (error) => { console.log('there was an error sending the query', error) }) } }
Lors de l'appel apollo.mutate
, nous devons fournir la mutation que nous appelons ainsi que les variables. Nous obtenons le résultat dans le rappel d' subscribe
et utilisons le router
injecté pour revenir à la liste des publications.
Réagir
export class FormStore { constructor() { this.appStore = AppStore.getInstance() this.routerStore = RouterStore.getInstance() this.postFormState = new PostFormState() } submit = async () => { await this.postFormState.form.validate() if (this.postFormState.form.error) return const result = await this.appStore.apolloClient.mutate( { mutation: AddPostMutation, variables: { name: this.appStore.username, title: this.postFormState.title.value, message: this.postFormState.message.value } } ) this.goBack() } goBack = () => { this.routerStore.history.push('/posts') } }
Très similaire à ci-dessus, avec la différence d'une injection de dépendance plus "manuelle" et de l'utilisation de async/await
.
Résumé : Encore une fois, pas beaucoup de différence ici. subscribe vs async/wait est fondamentalement tout ce qui diffère.
Formes
Nous voulons atteindre les objectifs suivants avec les formulaires de cette application :
- Liaison de données de champs à un modèle
- Messages de validation pour chaque champ, plusieurs règles
- Prise en charge de la vérification de la validité de l'ensemble du formulaire
Réagir
export const check = (validator, message, options) => (value) => (!validator(value, options) && message) export const checkRequired = (msg: string) => check(nonEmpty, msg) export class PostFormState { title = new FieldState('').validators( checkRequired('Title is required'), check(isLength, 'Title must be at least 4 characters long.', { min: 4 }), check(isLength, 'Title cannot be more than 24 characters long.', { max: 24 }), ) message = new FieldState('').validators( checkRequired('Message cannot be blank.'), check(isLength, 'Message is too short, minimum is 50 characters.', { min: 50 }), check(isLength, 'Message is too long, maximum is 1000 characters.', { max: 1000 }), ) form = new FormState({ title: this.title, message: this.message }) }
Ainsi, la bibliothèque formstate fonctionne comme suit : Pour chaque champ de votre formulaire, vous définissez un FieldState
. Le paramètre passé est la valeur initiale. La propriété validators
prend une fonction, qui renvoie "false" lorsque la valeur est valide, et un message de validation lorsque la valeur n'est pas valide. Avec les fonctions d'assistance check
et checkRequired
, tout peut sembler joliment déclaratif.
Pour avoir la validation pour l'ensemble du formulaire, il est avantageux d'envelopper également ces champs avec une instance FormState
, qui fournit ensuite la validité globale.
@inject('appStore', 'formStore') @observer export class FormComponent extends React.Component<FormComponentProps, any> { render() { const { appStore, formStore } = this.props const { postFormState } = formStore return <div> <h2> Create a new post </h2> <h3> You are now posting as {appStore.username} </h3> <Input type='text' label='Title' name='title' error={postFormState.title.error} value={postFormState.title.value} onChange={postFormState.title.onChange} /> <Input type='text' multiline={true} rows={3} label='Message' name='message' error={postFormState.message.error} value={postFormState.message.value} onChange={postFormState.message.onChange} />
L'instance FormState
fournit les propriétés value
, onChange
et error
, qui peuvent être facilement utilisées avec n'importe quel composant frontal.
<Button label='Cancel' onClick={formStore.goBack} raised accent /> <Button label='Submit' onClick={formStore.submit} raised disabled={postFormState.form.hasError} primary /> </div> } }
When form.hasError
is true
, we keep the button disabled. The submit button sends the form to the GraphQL mutation presented earlier.
Angular
In Angular, we are going to use FormService
and FormBuilder
, which are parts of the @angular/forms
package.
@Component({ selector: 'app-form', templateUrl: './form.component.html', providers: [ FormService ] }) export class FormComponent { postForm: FormGroup validationMessages = { 'title': { 'required': 'Title is required.', 'minlength': 'Title must be at least 4 characters long.', 'maxlength': 'Title cannot be more than 24 characters long.' }, 'message': { 'required': 'Message cannot be blank.', 'minlength': 'Message is too short, minimum is 50 characters', 'maxlength': 'Message is too long, maximum is 1000 characters' } }
First, let's define the validation messages.
constructor( private router: Router, private formService: FormService, public appService: AppService, private fb: FormBuilder, ) { this.createForm() } createForm() { this.postForm = this.fb.group({ title: ['', [Validators.required, Validators.minLength(4), Validators.maxLength(24)] ], message: ['', [Validators.required, Validators.minLength(50), Validators.maxLength(1000)] ], }) }
Using FormBuilder
, it's quite easy to create the form structure, even more succintly than in the React example.
get validationErrors() { const errors = {} Object.keys(this.postForm.controls).forEach(key => { errors[key] = '' const control = this.postForm.controls[key] if (control && !control.valid) { const messages = this.validationMessages[key] Object.keys(control.errors).forEach(error => { errors[key] += messages[error] + ' ' }) } }) return errors }
To get bindable validation messages to the right place, we need to do some processing. This code is taken from the official documentation, with a few small changes. Basically, in FormService, the fields keep reference just to active errors, identified by validator name, so we need to manually pair the required messages to affected fields. This is not entirely a drawback; it, for example, lends itself more easily to internationalization.
onSubmit({ value, valid }) { if (!valid) { return } this.formService.addPost(value) } onCancel() { this.router.navigate(['/posts']) } }
Again, when the form is valid, data can be sent to GraphQL mutation.
<h2> Create a new post </h2> <h3> You are now posting as {{appService.username}} </h3> <form [formGroup]="postForm" (ngSubmit)="onSubmit(postForm)" novalidate> <md-input-container> <input mdInput placeholder="Title" formControlName="title"> <md-error>{{validationErrors['title']}}</md-error> </md-input-container> <br> <br> <md-input-container> <textarea mdInput placeholder="Message" formControlName="message"></textarea> <md-error>{{validationErrors['message']}}</md-error> </md-input-container> <br> <br> <button md-raised-button (click)="onCancel()" color="warn">Cancel</button> <button md-raised-button type="submit" color="primary" [disabled]="postForm.dirty && !postForm.valid">Submit</button> <br> <br> </form>
The most important thing is to reference the formGroup we have created with the FormBuilder, which is the [formGroup]="postForm"
assignment. Fields inside the form are bound to the form model through the formControlName
property. Again, we disable the “Submit” button when the form is not valid. We also need to add the dirty check, because here, the non-dirty form can still be invalid. We want the initial state of the button to be “enabled” though.
Summary: This approach to forms in React and Angular is quite different on both validation and template fronts. The Angular approach involves a bit more “magic” instead of straightforward binding, but, on the other hand, is more complete and thorough.
Bundle size
Oh, one more thing. The production minified JS bundle sizes, with default settings from the application generators: notably Tree Shaking in React and AOT compilation in Angular.
- Angular: 1200 KB
- React: 300 KB
Well, not much surprise here. Angular has always been the bulkier one.
When using gzip, the sizes go down to 275kb and 127kb respectively.
Just keep in mind, this is basically all vendor libraries. The amount of actual application code is minimal by comparison, which is not the case in a real-world application. There, the ratio would be probably more like 1:2 than 1:4. Also, when you start including a lot of third-party libraries with React, the bundle size also tends to grow rather quickly.
Flexibility of Libraries vs. Robustness of Framework
So it seems that we have not been able (again!) to turn up a clear answer on whether Angular or React is better for web development.
It turns out that the development workflows in React and Angular can be very similar, depending on which libraries we chose to use React with. Then it's a mainly a matter of personal preference.
If you like ready-made stacks, powerful dependency injection and plan to use some RxJS goodies, chose Angular.
If you like to tinker and build your stack yourself, you like the straightforwardness of JSX and prefer simpler computable properties, choose React/MobX.
Again, you can get the complete source code of the application from this article here and here.
Or, if you prefer bigger, RealWorld examples:
- RealWorld Angular 4+
- RealWorld React/MobX
Choose Your Programming Paradigm First
Programming with React/MobX is actually more similar to Angular than with React/Redux. There are some notable differences in templates and dependency management, but they have the same mutable/data binding paradigm.
React/Redux with its immutable/unidirectional paradigm is a completely different beast.
Don't be fooled by the small footprint of the Redux library. It might be tiny, but it's a framework nevertheless. Most of the Redux best practices today are focused on using redux-compatible libraries, like Redux Saga for async code and data fetching, Redux Form for form management, Reselect for memorized selectors (Redux's computed values). and Recompose among others for more fine-grained lifecycle management. Also, there's a shift in Redux community from Immutable.js to Ramda or lodash/fp, which work with plain JS objects instead of converting them.
A nice example of modern Redux is the well-known React Boilerplate. It's a formidable development stack, but if you take a look at it, it is really very, very different from anything we have seen in this post so far.
I feel that Angular is getting a bit of unfair treatment from the more vocal part of JavaScript community. Many people who express dissatisfaction with it probably do not appreciate the immense shift that happened between the old AngularJS and today's Angular. In my opinion, it's a very clean and productive framework that would take the world by storm had it appeared 1-2 years earlier.
Still, Angular is gaining a solid foothold, especially in the corporate world, with big teams and needs for standardization and long-term support. Or to put it in another way, Angular is how Google engineers think web development should be done, if that still amounts to anything.
As for MobX, similar assessment applies. Really great, but underappreciated.
In conclusion: before choosing between React and Angular, choose your programming paradigm first.
mutable/data-binding or immutable/unidirectional , that… seems to be the real issue.