DEV Community

Jean-Noël for Younup

Posted on

J'ai essayé de passer à Vue.js depuis React.js

Introduction

Il y a quelques années, comme beaucoup d'autres, j'étais "hypé" par l'arrivée des hooks et les composants fonctionnels de la librairie frontend React.js. Ils proposaient une toute nouvelle manière de développer en écrivant beaucoup moins de code qu'avec les class Components. J'ai véritablement accroché, et pour un bon moment.

Aujourd'hui, j'ai dû opter pour le framework Vue.js afin de répondre aux besoins d'un tout nouveau projet client. Et étant arrivé au bout de ce projet, je me suis dit que c'était l'occasion de vous proposer un retour d'expérience en tant que nouvel utilisateur de ce framework !

Alors, est-ce que cette montée en compétence a été à la hauteur de la notoriété de Vue.js ?
Est-ce qu'il vaut mieux, aujourd'hui, développer du frontend en Vue qu'en React ?

C'est ce que nous allons voir !

Démarrage du projet

Boilerplate

Qui dit démarrage de projet, dit recherche d'un bon boilerplate pour nous épargner des heures, voire des jours de configuration laborieuse !
Sans avoir besoin de chercher très loin, la commande npm create vue@latest répond largement à mes besoins :

✔ Project name: … myproject-vue
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit Testing? … No / Yes
✔ Add an End-to-End Testing Solution? › Playwright
✔ Add ESLint for code quality? … No / Yes
✔ Add Prettier for code formatting? … No / Yes
? Add Vue DevTools 7 extension for debugging? (experimental) › No / Yes
Enter fullscreen mode Exit fullscreen mode

Le langage Typescript est déjà pris en charge, un système de routage et un store sont proposés, et il y a même de quoi réaliser des tests unitaires et End-to-End !

Par défaut, c'est le bundler Vite qui est installé. Ce qui n'est pas pour me déplaire !😄 En effet, les builds sont rapides et, la plupart du temps le Hot Module Replacement (HMR) fonctionne bien.

Un petit npm run dev pour lancer le serveur de dev local, et hop ! Ça tourne déjà dans le navigateur !

Vue.js 3 boilerplate

Et pour la mise en prod ? Il suffit de saisir la commande npm run build, et le projet s'exporte en fichiers statiques dans un répertoire dist après avoir vérifié les typages (dans le cas où l'on a activé le Typescript) :

vite v5.2.11 building for production...
✓ 48 modules transformed.
dist/index.html                      0.43 kB │ gzip:  0.28 kB
dist/assets/AboutView-C6Dx7pxG.css   0.09 kB │ gzip:  0.10 kB
dist/assets/index-D6pr4OYR.css       4.21 kB │ gzip:  1.30 kB
dist/assets/AboutView-CEwcYZ3g.js    0.22 kB │ gzip:  0.20 kB
dist/assets/index-CfPjtpcd.js       89.23 kB │ gzip: 35.29 kB
✓ built in 591ms
Enter fullscreen mode Exit fullscreen mode

Architecture du projet

.
├── README.md
├── e2e/
├── index.html
├── package.json
├── public/
├── src/
│   ├── App.vue
│   ├── assets/
│   ├── components/
│   │   ├── HelloWorld.vue
│   │   ├── TheWelcome.vue
│   │   ├── WelcomeItem.vue
│   │   ├── __tests__/
│   │   └── icons/
│   ├── main.ts
│   ├── router/
│   │   └── index.ts
│   ├── stores/
│   │   └── counter.ts
│   └── views/
│       ├── AboutView.vue
│       └── HomeView.vue
├── tsconfig.json
└── vite.config.ts
Enter fullscreen mode Exit fullscreen mode

Côté architecture du projet, on trouve notamment :

  • le fichier index.html, avec la balise <div id="app"></div> sur laquelle vient se greffer toute notre application Vue ;
  • le main.ts, avec la création successive du composant App, du router et du store :
import './assets/main.css';

import { createApp } from 'vue';
import { createPinia } from 'pinia';

import App from './App.vue';
import router from './router';

const app = createApp(App); // composant racine

app.use(createPinia()); // store
app.use(router); // routage des pages front

app.mount('#app');
Enter fullscreen mode Exit fullscreen mode
  • des fichiers .ts purs, pour gérer le routage et le store ;
  • quelques fichiers de config et de test ;
  • ... et bien sûr les fichiers *.vue, distingués en components (qui correspondent plutôt à des éléments génériques et réutilisables), et views (qui correspondent plutôt à des pages haut niveau)

En bref, l'architecture des fichiers est plutôt simple et relativement similaire à celle de React, même en ayant coché pas mal d'options dans le boilerplate.
Jusque-là, venant de React, rien de bien nouveau. C'est ensuite qu'apparaissent des différences significatives !

Architecture d'un fichier Vue

Voici un extrait de code inspiré du site officiel. Il se contente de changer la couleur du texte si l'on clique dessus et d'afficher la phrase "Le texte ci-dessus est vert", le cas échéant, mais il représente une architecture typique des fichiers *.vue :

<script setup>
  import { ref } from 'vue';

  const color = ref('green');

  function toggleColor() {
    color.value = color.value === 'green' ? 'blue' : 'green';
  }
</script>

<template>
  <p class="main-text" @click="toggleColor">
    Cliquez sur ce texte pour changer de couleur.
  </p>
  <p v-if="color === 'green'">Le texte ci-dessus est vert.</p>
</template>

<style scoped>
  .main-text {
    color: v-bind(color);
  }
</style>
Enter fullscreen mode Exit fullscreen mode

Notez le binding des évènements avec @click, l'affichage conditionnel avec le v-if, et le binding dans le CSS avec v-bind().

Le code est séparé en 3 parties bien distinctes :

  • script : le code de contrôle ;
  • template : la structure HTML ;
  • style : la feuille de style CSS.

Et ces trois parties ne se mélangent jamais 😮.
Cela présente plusieurs avantages que j'ai personnellement éprouvés tout au long mon expérience sur le projet client :

  • La structure HTML est claire, fixe, et dans un style très déclaratif - tout est là, même les balises affichées sous conditions ;
  • la partie logique est bien séparée de la partie affichage ;
  • on peut écrire du pur CSS en place, directement lié au composant, et sans installer de librairies tierce ;
  • malgré la séparation du style, on peut quand même insérer des variables dans le CSS.

Avec le tag scoped sur la balise <style>, on peut même générer automatiquement des classes locales afin d'isoler sa feuille de style !

Petit inconvénient : a priori il n'existe pas de polyfill CSS automatique. Il faut alors plutôt viser une librairie comme vue-emotion.

De mon point de vue, je trouve que ce genre de librairie "all-in-JS" casse un peu l'architecture que propose Vue, et de toute évidence, les propriétés CSS spécifiques aux navigateurs sont beaucoup plus rares de nos jours. La balise <style> de Vue est donc souvent autosuffisante.

Bref, j'ai trouvé très plaisante cette architecture tout-en-un mais avec des sections bien séparées. Cela permet de garder un code propre, mais aussi plus concis. En effet, la présence simultanée des 3 sections "logique métier / affichage / style" incite souvent à redécouper son code en plus petits modules, et donc en plus petits fichiers.

Maintenant, si on se penchait un peu plus sur l'API Vue.js elle-même ?

L'API Vue.js

Ici je ne vous ferai pas la liste exhaustive de tous les éléments de l'API Vue.js que j'ai pu rencontrer, mais seulement de certains, bien spécifiques, que j'ai trouvés assez représentatifs de la logique de Vue.

Les valeurs (re)calculées

Commençons par une opération bien connue de l'univers React : recalculer intelligemment un rendu HTML (ou une variable) suite à la mise à jour d'une donnée.
Il y a la fonction très intuitive computed() qui bénéficie d'un système de mémoïsation (sorte de "cache") pour éviter de recalculer à chaque fois la valeur de sortie :

Rendu du code vuejs computed

<script setup>
  import { ref, computed } from 'vue';

  const pushedBtn = ref(1);

  const magicNumber = computed(() => {
    console.log('computed again!');
    return pushedBtn.value * 21;
  });
</script>

<template>
  <button @click="pushedBtn = 1">Button 1</button>
  <button @click="pushedBtn = 2">Button 2</button>
  <p>Magic number: {{magicNumber}}</p>
</template>
Enter fullscreen mode Exit fullscreen mode

Ici le magicNumber est calculé uniquement si la valeur de pushedBtn vient à changer. Et c'est vérifiable : le message "computed again!" ne s'affiche dans la console que lorsqu'un bouton différent est cliqué.
Donc, contrairement à React, pas besoin de spécifier explicitement les variables qu'il faut surveiller dans cette fonction.

Dans la même lignée, on trouve également les watch et watchEffect qui permettent respectivement de réagir aux changements de tout ou partie des propriétés du composant, à l'image du useEffect en React :

<script setup>
  import { ref, watch } from 'vue';

  const num = ref(1);
  const text = ref('a');

  const countWatchs = ref(0);

  // Attente d'un changement de 'num'
  watch([num], () => {
    countWatchs.value++;
  });
</script>
<template>
  <button @click="num += 4">Modify num</button>
  <button @click="text += 'a'">Modify text</button>
  <p>Props:</p>
  <ul>
    <li>num: {{num}}</li>
    <li>text: {{text}}</li>
  </ul>
  <hr />
  <p>Nb watchs calls: {{countWatchs}}</p>
</template>
Enter fullscreen mode Exit fullscreen mode

Seul le premier bouton incrémente le compteur

Seul un clic sur le bouton modificateur de num va incrémenter le compteur de "watchs".
Le watch() nous permet alors de déclencher une callback à chaque fois que certaines variables changent.

La force de cette fonction réside dans l'analyse en profondeur des modifications de variable : Vue détecte les changements même au fin fond d'un sous-objet !

La synchronisation bidirectionnelle

Déclarer et transmettre une propriété d'un composant parent à un composant enfant est une opération assez récurrente. Synchroniser cette valeur entre l'enfant et le parent l'est également, par exemple dans l'input un formulaire.

Aussi, plutôt que de gérer à la fois une propriété et une callback de mise à jour sur évènement comme ici :

<!-- Child.vue -->
<script setup>
  const props = defineProps(['myModelValue']); // déclaration propriété
  const emit = defineEmits(['update:myModelValue']); // déclaration callback
</script>
<template>
  <h3>Textfield:</h3>
  <input
    :value="props.modelValue"
    @input="emit('update:myModelValue', 
     ($event.target as HTMLInputElement).value)"
  />
</template>
Enter fullscreen mode Exit fullscreen mode

… il est possible d'utiliser à la place la macro defineModel qui permet de simplifier l'écriture :

<!-- Child.vue -->
<script setup>
  const myModelValue = defineModel('myModel');
</script>
<template>
  <h3>Textfield:</h3>
  <input v-model="myModelValue" />
</template>
Enter fullscreen mode Exit fullscreen mode

Beaucoup plus court ! 😎 D'ailleurs, n'ayant qu'un seul modèle, j'aurais même pu me dispenser de le nommer !

Et avec le parent :

<!-- Parent.vue -->
<script setup>
  import { ref } from 'vue';
  import Child from './Child.vue';

  const data = ref('my default value');
</script>

<template>
  <div>Parent value: {{data}}</div>
  <Child v-model:my-model="data"></Child>
</template>
Enter fullscreen mode Exit fullscreen mode

Gif synchro input enfant et parent

La boucle for

Enchaîner sur les v-for après avoir vu les v-model m'a fait réaliser que Vue.js commençait à introduire pas mal de "magie" à travers du code implicite :

<script setup>
  const commonGitActions = ['Pull', 'Commit', 'Push', 'Merge', 'Rebase'];
</script>

<template>
  <h1>Actions git communes</h1>
  <ul>
    <!-- Boucle "for" intégrée -->
    <li v-for="action in commonGitActions">{{action}}</li>
  </ul>
</template>
Enter fullscreen mode Exit fullscreen mode

Et le rendu :

Vuejs3 v-for instruction

Comme on pouvait s'y attendre, l'instruction v-for permet donc de répéter automatiquement une partie d'un patron HTML (ici la balise <li>) pour chaque élément d'un itérable.

Côté React, il aurait fallu utiliser du JSX pour construire soi-même chaque élément, rendant le code moins lisible au fur et à mesure que le composant grandit :

import React from 'react';

const commonGitActions = ['Pull', 'Commit', 'Push', 'Merge', 'Rebase'];

export function App(props) {
  return (
    <>
      <h1>Actions git communes</h1>
      <ul>
        {commonGitActions.map((action) => (
          <li key={action}>{action}</li>
        ))}
      </ul>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Personnellement, j'ai une préférence pour la structure de Vue en termes de propreté de code, tant qu'il n'y a pas besoin de déboguer. 😅
Et d'ailleurs, puisqu'on parle de déboguer, qu'en est-il des outils de l'écosystème Vue ?

Les outils de dev

Voici 3 outils qui ont retenu mon attention dans le développement de mon projet.

Extension VSCode : Vue Official

Je commence par une évidence, mais oui, il y a bien une extension pour VSCode Vue (et pour d'autres IDEs) qui ajoute la coloration syntaxique, autocomplétion, snippets, etc. Un indispensable !

Toutefois, j'ai constaté quelques instabilités sur la coloration et l'autocomplétion, qui étaient parfois un peu capricieuses 😕, là où j'ai pu apprécier une plus grande stabilité côté React.

Vue.js devtools

À l'image du plugin de navigateur React Developer Tools, il existe le plugin Vue.js devtools, qui, je dois l'avouer, est déjà très bien fourni :

VueJS dev tools

On y retrouve 4 onglets :

  • Components, où on l'on peut observer, mais également modifier les états des composants ;
  • Timeline, qui permet d'enregistrer les évènements et les temps de rendu des composants, ce qui correspond en fait à une version de l'onglet "Performances" du navigateur mais focalisée sur Vue ;
  • Pinia, qui permet d'observer et modifier directement les états du store standard 😯, une intégration déjà toute prête que j'ai trouvée particulièrement bienvenue ;
  • Routes, où l'on peut lister les différentes routes et leur activité - c'est un onglet que j'ai trouvé un peu gadget sur mon projet de taille modérée (d'autant plus que les informations sont un peu redondantes avec celles de l'onglet "Components"), mais qui peut s'avérer bien utile sur un routage complexe.

Bref, pour du débogage, il y a tout ce qu'il faut et même plus !

Vuetify

Quasiment sans surprise (mais avec du mérite tout de même !), il existe aussi une librairie UI pour Vue qui implémente les Material Design de Google et fournit également une liste d'icônes standards : Vuetify.

<v-alert border='left' icon='$mdiVuetify' type='success'></v-alert>
Enter fullscreen mode Exit fullscreen mode

Rendu alerte Vuetify

Cela permet de gagner beaucoup de temps sur des démarrages de projet, ou sur des projets qui ne nécessitent pas de personnalisation graphique trop poussée.

Mais comme toujours, je recommande de garder un œil sur les performances de rendu avec ce genre de librairie haut niveau. La capacité de configuration d'une librairie se paie souvent ailleurs !

Conclusion

Que dire de cette expérience de migration de React vers Vue ?

Tout d'abord, d'un point de vue du code, par rapport à React je dirais que la lib Vue est :

  • plus structurée ;
  • plus déclarative ;
  • plus concise.

Toutefois, grâce à son code qui s'écrit plutôt en JSX, je trouve que React reste beaucoup plus interopérable, plus programmatique et plus explicite que Vue, et avec une meilleure stabilité du linter.

Côté environnement de développement et communauté, Vue a pour moi toutes les cartes en main pour assurer des développements efficaces jusqu'à la mise en prod.

Alors est-ce que cette montée en compétence sur Vue a été à la hauteur de sa notoriété ? Je dirais que oui. J'ai trouvé la courbe d'apprentissage efficace, et je continuerai de développer avec Vue si l'occasion se présente.

Enfin, est-ce qu'aujourd'hui il vaut mieux développer du frontend en Vue qu'en React ?
D'un point de vue totalement personnel, je pense que non. Même si Vue et React ont chacun des cas d'application un peu différents, je préfère me fier à un système de typage fiable et à un code plus souple avec React. Mais peut-être que les prochaines versions de Vue et leurs outils me feront changer d'avis ?

Et vous, quels sont vos retours d'expérience ?

Top comments (0)