29 avril 2024
Nouveautés de React 19
7 minutes de lecture
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.