Aller au contenu

Comment React 19 va simplifier votre vie de développeur Front-end

La sortie officielle de React 19 arrive à grands pas, êtes-vous prêt ?

Photo by Lautaro Andreani / Unsplash

React 19 est disponible en version Canary et sa version stable arrive prochainement.
Avec cette release, beaucoup de nouvelles fonctionnalités sont disponibles.
Certaines vont réduire le code boilerplate de nos composants, d'autres vont améliorer leur performance globale.

Voici un tour des principales nouveautés :

  1. Le compilateur React
  2. Les nouveaux hooks
    • a. use
    • b. useOptimistic
    • c. useFormStatus
    • d. useActionState
  1. Ref as a prop
  2. Petits changements notables

I. Le compilateur React

Le héros qu'on attendait tous

C'est un changement qui devrait grandement améliorer les performances de nos applications sans impacter d'un poil notre expérience de développeur.
J'ai nommé : le compilateur React

Prenons un exemple simple, imaginons un composant FruitsListContainer, appelant un composant Header et un composant FruitsList :

const FruitsListContainer = ({ title, fruits }) => {
  const uniqueFruits = [...new Set(fruits)];
  return (
    <>
      <Header title={title} />
      <FruitsList fruits={uniqueFruits} />
    </>
  );
};

const Header = ({ title }) => {
  return <h1>{title}</h1>;
};

const FruitsList = ({ fruits }) => {
  return (
    <ul>
      {fruits.map((fruit) => (
        <li key={fruit}>{fruit}</li>
      ))}
    </ul>
  );
};

export default FruitsListContainer;

Avec le code ci-dessus, la liste est affichée correctement.
Cependant, si l'un des props de FruitsListsContainer vient à changer, tous les sous-composants seront régénérés.
Par exemple : si le titre est modifié mais la liste des fruits ne l'est pas, le composant Header et FruitsList seront tous les deux régénérés.

Pour palier ce problème, il existe une solution qui consiste à utiliser useMemo et useCallback. Deux hooks spécifiquement conçus pour ce cas précis.
Mais l'expérience développeur se dégrade car le code est plus compliqué à lire.

Avec le compilateur React, plus besoin de useMemo, ni de useCallback. Tout est géré automatiquement !

Bien que le compilateur soit actuellement utilisé en production chez Meta, notamment pour Instagram, il ne sera disponible en version stable qu'avec React 19.


II. Les nouveaux hooks

a. use

Le nouveau hook magique

Vous en avez marre d'utiliser useState et useEffect à tout bout de champ dans votre code pour effectuer des actions simples ?
Alors le hook use est fait pour vous !

Ce nouveau hook permet de lire une ressource (une promesse ou un contexte, par exemple), d'en lire la valeur et de demander le render du composant à la modification de cette dernière.

Cela permet notamment de remplacer l'utilisation du useState et du useEffect dans plusieurs cas.
Par exemple, celui de la récupération d'une valeur de manière asynchrone au chargement du composant.

Un peu de pratique !

Le blabla c'est bien beau, mais soyons concrets.

Créons ensemble un composant affichant une image de chat aléatoire sur notre page web.

Voici comment nous pouvons implémenter cette solution sans le hook use

Créons d'abord une fonction récupérant l'URL aléatoire vers une image de chat :

const fetchRandomCatObject = async () => {
  const res = await fetch("https://api.thecatapi.com/v1/images/search");
  return await res.json();
};

Ensuite, créons un composant appelant cette fonction, stockant sa valeur dans un state et affichant l'image associée :

const CatImg = () => {
  // On stocke l'url de l'image dans un state
  const [catImgUrl, setCatImgUrl] = useState();

  // On utilise un useEffect pour récupérer une seule fois l'image, au chargement du composant
  useEffect(() => {
    fetchRandomCatObject().then(([cat]) => {
      setCatImgUrl(cat?.url);
    });
  }, []);
  return (
    <>
      {catImgUrl ? (
        <img src={catImgUrl} alt="Image of a random cat" />
      ) : (
        <p>Loading...</p>
      )}
    </>
  );
};

Et enfin, créons un dernier composant pour finaliser notre page. Voici le code complet :

import { useEffect, useState } from "react";

// Fonction de récupération de l'image
const fetchRandomCatObject = async () => {
  const res = await fetch("https://api.thecatapi.com/v1/images/search");
  return await res.json();
};

// Sous composant se chargeant de l'affichage de l'image
const CatImg = () => {
  // On stocke l'url de l'image dans un state
  const [catImgUrl, setCatImgUrl] = useState();

  // On utilise un useEffect pour récupérer une seule fois l'image, au chargement du composant
  useEffect(() => {
    fetchRandomCatObject().then(([cat]) => {
      setCatImgUrl(cat?.url);
    });
  }, []);
  return (
    <>
      {catImgUrl ? (
        <img src={catImgUrl} alt="Image of a random cat" />
      ) : (
        <p>Loading...</p>
      )}
    </>
  );
};

// Composant principal
const RandomCatImg = () => {
  // Enfin, on affiche le composant en charge de l'affichage de l'image
  return <CatImg />;
};

export default RandomCatImg;

Et maintenant, avec le nouveau hook use
Avec use, bye bye le useState et le useEffect.

Revoyons notre fonction de récupération de l'image afin qu'elle retourne directement l'URL :

const fetchRandomCatUrl = async () => {
  const res = await fetch("https://api.thecatapi.com/v1/images/search");
  const [cat] = await res.json();
  return cat?.url ?? "";
};

Le sous-composant peut maintenant se passer du state, et par extension, du useEffect également :

const CatImg = () => {
  const catImgUrl = use(fetchRandomCatUrl());
  return <img src={catImgUrl} alt="Image of a random cat" />;
};

Notre code final est beaucoup moins long et plus simple à lire :

import { use, Suspense } from "react";

// Fonction de récupération de l'image
const fetchRandomCatUrl = async () => {
  const res = await fetch("https://api.thecatapi.com/v1/images/search");
  const [cat] = await res.json();
  return cat?.url ?? "";
};

// Sous composant se chargeant de l'affichage de l'image
const CatImg = () => {
  const catImgUrl = use(fetchRandomCatUrl());
  return <img src={catImgUrl} alt="Image of a random cat" />;
};

// Composant principal
const RandomCatImg = () => {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <CatImg />
    </Suspense>
  );
};

export default RandomCatImg;

b. useOptimistic

Un hook pour la modification optimiste de composants

Il s'agit la d'un hook un peu spécifique mais qui peut s'avérer très utile.
Imaginons que vous construisez une application de messagerie instantanée.
À chaque fois que vous envoyez un message, ce dernier est envoyé à un serveur afin d'être distribué aux autres utilisateurs.

Afin d'améliorer l'expérience de l'utilisateur, vous souhaitez mettre en place un indicateur lorsque le message est en cours d'envoi.
Voici comment vous pouvez faire cela avec useOptimistic.

Commençons par créer un composant principal contenant la liste des messages, ainsi que la fonction d'envoi d'un nouveau message :

const OptimisticChat = () => {
  const [messages, setMessages] = useState([
    {
      author: "Serge",
      content: "Vous avez vu le nouveau hook useOptimistic ?",
    },
    { author: "Marie", content: "Wow c'est vraiment incroyable" },
  ]);

  const sendMessage = async (newMessage) => {
    // Utilisation d'un setTimeout pour simuler l'envoi à un back-end
    await new Promise((res) => setTimeout(res, 1000));
    setMessages((currentMessages) => [
      ...currentMessages,
      { author: "me", content: newMessage, sending: false },
    ]);
  };

  return (
    <OptimisticChatContainer messages={messages} sendMessage={sendMessage} />
  );
};

Puis, créons le composant OptimisticChatContainer qui prend les messages dans ses props et permet d'envoyer un nouveau message en appelant la méthode sendMessage créée précédemment.

const OptimisticChatContainer = ({ messages, sendMessage }) => {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [
      ...state,
      { author: "me", content: newMessage, sending: true },
    ]
  );

  const handleMessageSend = async (newMessage) => {
    // Lorsqu'on envoie un nouveau message
    // Un message optimiste est d'abord créé
    addOptimisticMessage(newMessage);

    // Lorsque la méthode sendMessage aura renvoyé un résultat
    // optimisticMessages sera écrasé avec la valeur de messages
    await sendMessage(newMessage);
  };

  return (
    <div className="container">
      <Chat messages={optimisticMessages} />
      <ChatForm onMessageSend={handleMessageSend} />
    </div>
  );
};

Et enfin, créons les composants qui permettent :

  • D'afficher les messages
  • D'envoyer un nouveau message
const ChatForm = ({ onMessageSend }) => {
  const messageRef = createRef();
  const [isDisabled, setIsDisabled] = useState(true);

  const handleMessageChange = () => {
    setIsDisabled(messageRef.current?.value?.trim() === "");
  };

  const handleFormSubmit = async () => {
    const message = messageRef.current?.value?.trim();
    if (message !== "") {
      await onMessageSend(message);
    }
  };

  return (
    <form action={handleFormSubmit} className="chat-form">
      <input
        onChange={handleMessageChange}
        type="text"
        placeholder="Tapez un message..."
        ref={messageRef}
      />
      <button type="submit" disabled={isDisabled}>
        Envoyer
      </button>
    </form>
  );
};

const Chat = ({ messages }) => {
  return (
    <div className="chat">
      {Array.isArray(messages) &&
        messages.map((message, index) => (
          <ChatMessage
            key={index}
            author={message.author}
            content={message.content}
            sending={message.sending}
          />
        ))}
    </div>
  );
};

const ChatMessage = ({ author, content, sending }) => {
  return (
    <div className={`message ${author === "me" ? "me" : ""}`}>
      <span className="author">{author === "me" ? "Moi" : author}</span>
      <span className="content">{content}</span>
      <span className="sendingIndicator">{sending ? "sending..." : null}</span>
    </div>
  );
};

Ainsi, lorsqu'un message sera envoyé, un indicateur "Envoi en cours..." s'affichera tant que le message n'est pas totalement envoyé.

L'indicateur apparaît en dessous du message en cours d'envoi

c. useFormStatus

Ou comment mieux connaitre l'état de son formulaire.

Si vous avez déjà utilisé des formulaires natifs avec React, vous vous êtes sûrement retrouvé à devoir maintenir de la logique complexe pour effectuer des actions simples.
L'une d'entre elles est la gestion du chargement lors de l'envoi du formulaire.

Imaginons un formulaire de création de compte.
Lorsqu'on clique sur le bouton de validation du formulaire, ce dernier envoie les données au serveur. Mais cela prend plusieurs secondes...
Dans ce cas, on aimerait pouvoir désactiver le bouton et indiquer à l'utilisateur que le traitement est en cours.

Il devient plutôt simple d'effectuer cela avec useFormStatus.

Commençons par créer un composant contenant la balise <form> :

const SignUpForm = () => {
  const [isSignedUp, setIsSignedUp] = useState(false);
  
  const submitForm = async (data) => {
    // On simule une lenteur dans l'envoi du formulaire (3 secondes)
    return new Promise((resolve) => {
      setTimeout(() => {
        setIsSignedUp(true);
        resolve(data);
      }, 3000);
    });
  };

  return (
    <form action={submitForm} className="signup-form">
      <h1>Créer un compte</h1>
      {isSignedUp ? (
        <p>Vous êtes maintenant enregistré !</p>
      ) : (
        <SignUpFormContent />
      )}
    </form>
  );
};

Ce composant contient également un state indiquant si l'utilisateur a créé son compte ou non.
Ainsi qu'une méthode qui sera exécutée à la validation du formulaire.

Notez que le contenu du formulaire se trouve dans un sous-composant nommé SignUpContent.
Cela est primordial pour le fonctionnement de useFormStatus.

Créons ensuite le composant SignUpContent en y ajoutant les différents champs constituant le formulaire :

const SignUpFormContent = () => {
  const { pending, data } = useFormStatus();
  return (
    <>
      <div className="input-group">
        <label htmlFor="firstName">Prénom</label>
        <input
          type="text"
          name="firstName"
          placeholder="Jean-Claude"
          required
        />
      </div>
      <div className="input-group">
        <label htmlFor="lastName">Nom de famille</label>
        <input type="text" name="lastName" placeholder="Van Damme" required />
      </div>
      <div className="input-group">
        <label htmlFor="email">Email</label>
        <input
          type="email"
          name="email"
          placeholder="jcvd@gmail.com"
          required
        />
      </div>
      <div className="input-group">
        <label htmlFor="password">Mot de passe</label>
        <input
          type="password"
          name="password"
          placeholder="Mot de passe"
          required
        />
      </div>
      <div className="input-group">
        <input
          type="submit"
          value={pending ? "Chargement..." : "Créer un compte"}
          disabled={pending}
        />
      </div>
      {pending && (
        <p>
          Ravi de vous voir 
          <strong>
            {data?.get("firstName")} {data?.get("lastName")}
          </strong>
        </p>
      )}
    </>
  );
};

Dans cet exemple, nous récupérons deux informations du formulaire via useFormStatus :

  • pending : indique si le formulaire est en cours de soumission
  • data : contient les données actuelles du formulaire

Ainsi, lorsque le formulaire est en cours de soumission, le bouton est désactivé et un message apparaît en bas de l'écran.
Sans créer de state pour gérer l'envoi en cours du formulaire !


d. useActionState

Cette version nous offre également la possibilité de gérer les actions de nos formulaires via un nouveau hook useActionState.
Ce hook permet de stocker une valeur par défaut et d'appeler une fonction asynchrone à la soumission du formulaire.

Voici un exemple de l'utilisation de ce nouveau hook :

import { useActionState } from "react";

// Fonction asynchrone en charge de l'action à la soumission du formulaire
const increment = async (previousNumber, formData) => {
  return previousNumber + Number(formData.get("number"));
};

const IncrementForm = () => {
  // Utilisation du useActionState
  // On lui passe la fonction d'action ainsi que la valeur par défaut
  const [currentNumber, incrementFormAction] = useActionState(increment, 0);
  
  return (
    <form action={incrementFormAction}>
      <p>{currentNumber}</p>
      <input type="number" name="number" defaultValue="1" />
      <button type="submit">Incrémenter</button>
    </form>
  );
};

export default IncrementForm;

Avec cette technique, on évite de devoir créer une fonction intermédiaire appelant la fonction increment et mettant à jour un state contenant la valeur du compteur.


III. Ref as a prop

Ce n'est pas qu'un au revoir pour forwardRef

forwardRef permet d'exposer la référence d'un élément du DOM au composant parent.
La syntaxe du forwardRef est la suivante dans la version 18 de React :

import { forwardRef } from "react";

const button = forwardRef(props, ref) => (
  <button ref={ref}>
    {props.children}
  </button>
));

Avec le ref as a prop, forwardRef sera déprécié puis tout simplement supprimé dans une future version de React.

La référence peut maintenant être passée dans les props du composant, comme ceci :

const button = ({ ref, children }) => (
  <button ref={ref}>
	{children}
  </button>
));

IV. Petits changements notables

Les balises <meta>

Il sera désormais possible d'intégrer des balises HTML <meta> directement dans un composant React.
Cette balise sera automatiquement placée dans l'entête du document HTML généré.

<meta name="description" content="Ma super description" />

Changements de l'utilisation des contextes providers

Il sera maintenant possible d'utiliser un contexte directement au lieu de son provider.

Ainsi, le code suivant :

const ThemeContext = createContext('');

function App({children}) {
  return (
    <ThemeContext.Provider value="dark">
      {children}
    </ThemeContext.Provider>
  );  
}

Peut être simplifié par :

const ThemeContext = createContext('');

function App({children}) {
  return (
    <ThemeContext value="dark">
      {children}
    </ThemeContext>
  );  
}

Les refs cleanups

Lors du passage d'un ref à un composant, il est désormais possible de fournir une fonction de "nettoyage" de ce dernier.
Cela se fait en retournant une fonction comme ceci :

<input
  ref={(ref) => {
    // Lorsque l'élément lié à la ref est retiré du DOM
    // La fonction retournée ci-dessous sera exécutée
    return () => {
      // ref cleanup
    };
  }}
/>

La valeur par défaut pour useDeferredValue

Si vous connaissez useDeferredValue, un hook permettant de conserver en mémoire l'ancienne valeur d'un state pendant sa mise à jour. Afin de ne pas bloquer l'UI.
Sachez qu'il sera maintenant possible de lui fournir une valeur par défaut comme ceci :

const deferredValue = useDeferredValue(value, 'Valeur au premier render du composant');

Comment tester React 19 ?

Vous pouvez tester React 19 en créant un nouveau projet React et en installant la dernière version Canary de react (RC) depuis npmjs.org.
Puis en installant la version RC de react-dom depuis npmjs.org.

Vous pouvez également retrouver l'ensemble du code utilisé dans cet article sur ce lien vers un environnement CodeSandbox.

Vous trouverez également l'ensemble des nouvelles fonctionnalités sur le site officiel de React.


Pour conclure

Cette prochaine version apporte beaucoup de changements.
Ils sont plus ou moins gros mais tous très intéressants.
Nous n'avons couvert que le plus gros des évolutions de React, sans parler des fonctionnalités qui impacteront les différents frameworks basés sur React (notamment les server components et les server actions).

Pensez à consulter la documentation de React lors de la sortie officielle de la version stable, car de nombreux changements sont ajoutés au fil de l'eau.

Dernier