AccueilClientsExpertisesBlogOpen SourceJobsContact

29 avril 2024

Nouveautés de React 19

7 minutes de lecture

Nouveautés de React 19
🇺🇸 This post is also available in english

Après plus de 2 ans depuis sa dernière release officielle, l'équipe de Meta vient d'annoncer la version 19 de React en bêta. Découvrons les principales nouveautés apportées par cette version.

Nouvelle fonction use

La fonction use nous permet de récupérer des valeurs depuis une promesse ou depuis un contexte. Elle a la particularité, contrairement à un hook de pouvoir être appelée de manière conditionnelle sans provoquer d'erreur.

Utilisation dans le cadre d'une promesse

Dans le cadre de l'utilisation avec une promesse, la fonction use est accompagnée du composant Suspense. Le rendu sera suspendu le temps que la promesse soit résolue (ou rejetée). Prenons par exemple le rendu d'une liste provenant d'une API :

// App.tsx
import { Suspense } from 'react'
import Users from './Users'

function App() {
  const usersPromise = fetch('https://jsonplaceholder.typicode.com/users').then((res) => res.json())

  return (
    <Suspense fallback={<div>Loading</div>}>
      <Users usersPromise={usersPromise} />;
    </Suspense>
  )
}

export default App
// Users.tsx
'use client'
import { use } from 'react'

const Users = ({ usersPromise }: { usersPromise: Promise<{ id: number; name: string }[]> }) => {
  const users = use(usersPromise)

  return (
    <ul>
      {users.map((user) => {
        return <li key={user.id}>{user.name}</li>
      })}
    </ul>
  )
}

export default Users

Dans cet exemple, la promesse est transmise d'un Server Component vers un Client Component. Dans ce cas là, il faudra s'assurer que la promesse résolve des types de données sérialisables. Il est recommandé de créer la promesse dans le composant serveur et de la résoudre avec use dans le composant client. Pour les composants serveur, l'utilisation de async / await est préconisée par rapport à l'utilisation de use, qui va provoquer un re-render après la résolution de la promesse.

Gestion d'erreur

Pour gérer les erreurs, on utilisera le concept d'error boundary

import { Suspense } from 'react'
import Users from './Users'
import { ErrorBoundary } from 'react-error-boundary'

function App() {
  const usersPromise = new Promise((resolve, reject) => {
    setTimeout(() => reject(), 1000)
  })

  return (
    <ErrorBoundary fallback={<div>Une erreur est survenue</div>}>
      <Suspense fallback={<div>Loading</div>}>
        <Users usersPromise={usersPromise} />;
      </Suspense>
    </ErrorBoundary>
  )
}

export default App

Utilisation dans le cadre d'un context

La fonction use peut aussi être utilisée avec un contexte React. Pour cela rien de plus simple, on passe juste notre contexte en argument de notre fonction.

import { createContext, use } from 'react'

const TodoContext = createContext<string[]>([])

function Todos() {
  const todos = use(TodoContext)

  return (
    <ul>
      {todos.map((todo) => {
        return <li key={todo}>{todo}</li>
      })}
    </ul>
  )
}

function App() {
  return (
    <TodoContext value={['Make a React 19 article', 'Contribute to Next-Admin']}>
      <Todos />
    </TodoContext>
  )
}

export default App

On peut aussi noter une nouveauté de React dans cet exemple : il n'est plus nécessaire d'utiliser MonContext.Provider pour instancier le provider de notre contexte. Seul MonContext suffit.

Nouveautés pour les refs

Les refs bénéficient de quelques nouveautés. Pour rappel, une ref est une prop permettant d'obtenir l'instance d'un élément. À l'heure actuelle pour passer une ref vers un élément issu d'un composant fonctionnel, il faut encapsuler ce dernier avec la fonction forwardRef. Dans la version 19, ref devient une prop d'un composant fonctionnel et il n'est plus nécessaire d'utiliser forwardRef. Cependant, le comportement ne change pas pour les class components.

import { forwardRef } from 'react'

const ComponentWithRef = forwardRef((props, ref) => {
  return <p ref={ref}>Hello</p>
})

const ComponentWithRefProp = ({ ref }) => {
  return <p ref={ref}>Hello</p>
}

À celà s'ajoute une autre nouveauté : il est possible de savoir quand un élément est supprimé de notre DOM grâce à une fonction cleanup dans notre ref.

<div
  ref={(divRef) => {
    maRef.current = divRef

    // Exécuté quand le noeud est supprimé du DOM
    return () => {
      console.log('Div supprimée')
    }
  }}
>
  Ma div
</div>

Nouveau hook useOptimistic

Dans le cadre de requête asynchrone impliquant une mutation de données (par exemple un formulaire d'édition), il peut arriver de vouloir refléter instantanément les changements effectués sans attendre le retour du serveur. C'est un pattern que l'on retrouve par exemple dans react-query. Le hook attend deux arguments :

  • l'état "réel", c'est-à-dire l'état provenant de notre source de données
  • une fonction de mise à jour de l'état "temporaire", c'est-à-dire notre état affiché tant que la source donnée n'a pas renvoyé son nouvel état.
import { useOptimistic, useState } from 'react'

type Todo = {
  label: string
  isSending: boolean
}

function Todos({ todos, sendTodo }: { todos: Todo[]; sendTodo: (todo: string) => Promise<void> }) {
  const [optimisticTodos, addTodo] = useOptimistic(todos, (state, newValue) => [
    ...state,
    {
      label: newValue,
      isSending: true,
    },
  ])

  const onSubmit = async (formData: FormData) => {
    const todo = formData.get('todo')
    addTodo(todo)
    await sendTodo(todo)
  }

  return (
    <div>
      <ul>
        {optimisticTodos.map((todo: Todo, index: number) => {
          return (
            <li key={index}>
              {todo.label} {todo.isSending && '(sending...)'}
            </li>
          )
        })}
      </ul>
      <form action={onSubmit}>
        <input type="text" placeholder="Todo" name="todo" />
        <button type="submit">Submit</button>
      </form>
    </div>
  )
}

function App() {
  const [todos, setTodos] = useState<Todo[]>([])

  const addTodo = async (todo: string) => {
    await new Promise((resolve) => setTimeout(resolve, 1000))
    setTodos((old) => [...old, { label: todo, isSending: false }])
  }

  return <Todos todos={todos} sendTodo={addTodo} />
}

export default App

Nouveau hook useActionState

En reprenant notre exemple ci-dessus, nous pouvons par exemple vouloir afficher un retour visuel plus impactant au niveau du formulaire quand celui-ci est en état de chargement. Bien que l'on puisse faire cela simplement en parcourant notre tableau optimisticTodos à la recherche d'une propriété isSending à true, useActionState nous offre une solution plus élégante. Ce hook nous permet d'exécuter une fonction (que l'on nommera action) et d'obtenir un nouvel état à partir de cette action. On pourra aussi obtenir l'état de chargement de notre fonction, ainsi qu'un état qui sera fortement lié à notre action. Notre composant peut donc être grandement simplifié :

import { useOptimistic, useState, useActionState } from 'react'

type Todo = {
  label: string
  isSending: boolean
}

function App() {
  const [todos, onSubmitTodo, isPending] = useActionState(
    /**
     * Notre action
     */
    async (prevState: Todo[], newValue: string) => {
      await new Promise((resolve) => setTimeout(resolve, 1000))
      /**
       * Nouvel état
       */
      return [...prevState, { label: newValue, isSending: false }]
    },
    []
  )

  const [optimisticTodos, addTodo] = useOptimistic(todos, (state, newValue) => [
    ...state,
    {
      label: newValue,
      isSending: true,
    },
  ])

  const onSubmit = async (formData: FormData) => {
    const todo = formData.get('todo')
    addTodo(todo)
    await onSubmitTodo(todo)
  }

  return (
    <div>
      <ul>
        {optimisticTodos.map((todo: Todo, index: number) => {
          return (
            <li key={index}>
              {todo.label} {todo.isSending && '(sending...)'}
            </li>
          )
        })}
      </ul>
      <form action={onSubmit}>
        <input type="text" placeholder="Todo" name="todo" />
        <button type="submit" disabled={isPending}>
          Submit {isPending && '(loading...)'}
        </button>
      </form>
    </div>
  )
}

export default App

Support des balises meta

Dans le cadre d'une application, il arrive fréquemment que l'on souhaite insérer des balises meta bien que notre composant se situe à un niveau ne permettant pas d'accéder au head de notre document HTML. Des librairies telles que react-helmet permettaient de palier à ce soucis, mais leur comportement était assez fragile dans le cadre d'un rendu serveur. React 19 permet maintenant de rendre des balises meta au sein d'un composant, qui seront automatiquement rendues au sein du head du document. Ainsi :

const App = () => {
  return (
    <main>
      <title>Todo list</title>
      <p>My todo list</p>
    </main>
  )
}

export default App

rendra

<html>
  <head>
    <title>Todo list</title>
  </head>
  <body>
    <main>
      <p>My todo list</p>
    </main>
  </body>
</html>

Les Server Components et Server Actions

Déjà bien implémentés dans Next.JS, les Server Components et les Server Actions passeront dans un état stable dans la version 19. Nous avions eu la possibilité d'aborder plus en détail ces deux sujets :

Le grand absent: React Compiler

Un outil très attendu de cette release et qui ne verra pas le jour pour le moment : React Compiler. Pour rappel, son but est que le développeur n'ait plus à se soucier de la mémoisation via les différents hooks useMemo et useCallback. À la place, React Compiler s'occuperait de gérer automatiquement celle ci. Cependant, il semblerait que l'outil puisse arriver en open source très bientôt. Affaire à suivre, mais c'est une bonne chose de voir que le projet n'a pas été abandonné et est développé de manière active.

Conclusion

Avec cette version 19, React nous propose de nouvelles fonctionnalités simplifiant grandement certains pattern liés notamment à l'asynchrone. Il devient de plus en plus simple d'offrir une interface utilisateur fluide et rapide, tout en gardant une expérience de développement plus que correcte. Bien que les différentes fonctionnalités présentées étaient déjà présentes sur le canal canary de la version 18, il est possible de les tester sur les canaux canary, next et beta qui pointent tous les trois vers la version 19 bêta. Vous pourrez retrouver toutes les nouveautés de la version 19 sur l'article de blog de React.

18 avenue Parmentier
75011 Paris
+33 1 43 57 39 11
hello@premieroctet.com

Suivez nos aventures

GitHub
Twitter
Flux RSS

Naviguez à vue