Migrer une application React 17 vers 18

| 4 min de lecture

Chez Pubstack, on a récemment migré notre frontend de VueJS 2 à React. Lors de notre réécriture, la dernière version de React était la 17. Maintenant et depuis quelques mois, React est passé en version 18.

Notre setup :

Je vais vous expliquer ici comment faire la migration et les problèmes que nous avons rencontrés.

Comment faire ?

Mettre à jour le package.json avec les dernières versions de React 18 (18.2.0 au moment où j'écris cet article).

"react": "^18.2.0",
"react-dom": "^18.2.0",

Si vous utilisez Typescript (ce que je recommande fortement), il faut aussi mettre à jour les packages de type comme ci-dessous :

"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",

Si vous lancez l'application, vous allez voir apparaitre une erreur dans la console :

L'erreur dans la console du navigateur!

React 18 introduit une nouvelle manière d'instancier l'application (cf : https://reactjs.org/link/switch-to-createroot), il faut alors modifier le main.jsx|tsx.

En React 17, on avait :

import React, { useEffect } from 'react'
import ReactDOM from 'react-dom'

ReactDOM.render(
    <App />, 
    document.getElementById('root')
)

Maintenant, en React 18, cela devient :

import React, { useEffect } from 'react'
import { createRoot } from 'react-dom/client'

const container = document.getElementById('root')
const root = createRoot(container)

root.render(<App />)

Si vous utilisez Typescript, remplacez

const root = createRoot(container)

par

const root = createRoot(container!)

En Typescript, le ! après une variable est appelé "bang operator" ou "postfix bang". Il sert à indiquer à Typescript qu'une variable nullish (qui peut être null ou undefined) ne peut pas l'être à cet endroit.

Par exemple :

const variableCanBeNull: string | null = 'a'
// variableCanBeNull est de type string OU null
const variableNotNull = variableCanBeNull!
// variableNotNull est forcément de type string

D'après la documentation de Typescript, il est important d'utiliser le ! uniquement quand on sait qu'une variable ne peut pas être null ou undefined.

Dans notre cas, le type de retour document.getElementById est HTMLElement | null or le createContainer n'accepte pas de variable avec un type nullish.
Il faut donc utiliser le bang operator.

Vite

Pour ceux qui utilisent Vite, vous pouvez aussi sûrement observer dans les logs de build / run des warning de Vite qui ressemblent à ça :

 [vite] warning: Top-level "this" will be replaced with undefined since this file is an ECMAScript module
196|          lineNumber: 127,
197|          columnNumber: 9
198|        }, this), _jsxDEV(Link, {
   |           ^
199|          label: "Back",
200|          onClick: onSitesBackClick

  Plugin: vite:esbuild

Le plugin React de Vite utilise @babel/plugin-transform-react-jsx pour transformer le JSX et c'est ce dernier qui génère les warning. Une issue a été ouverte sur Github.

Si cela est gênant pour vous, pour l'instant, le mieux reste de les ignorer. C'est possible de le faire en modifiant la configuration de Vite comme ceci :

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  esbuild: {
    logOverride: { 'this-is-undefined-in-esm': 'silent' }
  }
})

Typescript

Depuis React 18, les types ont changé. Sebastian Silbermann, un contributeur des types React, a écrit dans une des PRs les breakings changes.

Récapitulons :

  • Suppression des implicit children, je vous conseille un très bon article de blog en Anglais qui explique pourquoi ils les ont retirés
  • this.context devient unknown à la place de any
  • Les types dépréciés suivants ont été retirés :
-StatelessComponent
+FunctionComponent

-SFC
+FC

-React.ReactType
+React.ElementType

-SFCElement
+FunctionComponentElement

// no replacement
-Props
-RefForwardingComponent
-SFCFactory

Alors, vous allez me dire, mais je n'ai pas que deux fichiers, ça va me prendre énormément de temps de tout modifier à la main et vous auriez raison !

C'est pour cela que cette même personne a fait un codemod pour transformer votre code .tsx React 17 en code .tsx React 18. Il est même possible de choisir quelles règles de transformation appliquer.

Sans plus attendre voici le lien !

Pour exécuter le codemod pour React 18 :

npx types-react-codemod preset-18 ./src

Je vous conseille de sélectionner toutes les rules.
On avait beaucoup de implicit-children dans notre code.

Par exemple sur le Button, voilà le résultat :

- const _Button: React.FC<ButtonProps> = ({ variant = 'primary', children, className, iconName, iconSize, ...props }) => { 
+ const _Button: React.FC<React.PropsWithChildren<ButtonProps>> = ({ variant = 'primary', children, className, iconName, iconSize, ...props }) => {

Attention, le script n'est pas réentrant. Si vous le relancez vous obtiendrez :

const _Button: React.FC<React.PropsWithChildren<React.PropsWithChildren<ButtonProps>>> = ({ variant = 'primary', children, className, iconName, iconSize, ...props }) => {

React 19

Avec le passage à React 18, de nouveaux types ont été dépréciés et seront retirés avec React 19, par exemple l'interface VoidFunctionComponent.

Vous pouvez aussi passer le script afin de modifier directement les types et d'être déjà prêt pour la prochaine release de React 🎉.

Allez, on le fait :

npx types-react-codemod preset-19 ./src

Le script a transformé mon composant Breadcrumbs comme ceci :

- const _Breadcrumbs: React.VoidFunctionComponent<BreadcrumbsProps> = ({ data, onClick, className }) => (
+ const _Breadcrumbs: React.FunctionComponent<BreadcrumbsProps> = ({ data, onClick, className }) => (

⚠️ Vérifiez votre application ⚠️

De notre côté, on a fait une vérification totale de notre application et on a eu une petite surprise.

En effet, on utilise emotion-js pour styliser nos composants, j'y reviendrai dessus lors d'un prochain article. On utilise aussi google-chart avec sa lib pour react react-google-charts pour dessiner des graphiques.

Sur nos graphiques, on a des tooltips, sauf qu'on souhaite les styliser et donc on ne veut pas ceux qui sont générés automatiquement. Heureusement, on peut lui passer via un argument, le tooltip que la lib doit afficher.

Jusque-là tout va bien, sauf que la prop prend du code HTML et non un composant React. Pour convertir le composant en HTML pur, nous utilisons :

ReactDOMServer.renderToString(tooltipContent)

Cela fonctionnait en React 17, mais depuis React 18, avec Emotion, le style n'est plus inclus et notre tooltip se retrouve sans style.

J'ai ouvert une issue côté Emotion.

Conclusion

Alors qu'a-t-on appris ici ?

  • Nouvelle manière d'instancier une application React
  • Quelques types dépréciés
  • Un codemod pour mettre à jour le code automatiquement
  • Des warnings pour les utilisateurs de Vite
  • Bien vérifier après la migration même si tout le code build, il peut y avoir des régressions insoupçonnées

Pour en savoir plus sur toutes les nouveautés de React 19, vous pouvez aller lire l'article du blog officiel (en anglais).

Si vous constatez une coquille dans l'article, n'hésitez pas à me contacter sur Twitter. De la même manière, si vous souhaitez être au courant des prochains articles, vous pouvez me suivre sur Twitter.