← Retour aux articles
React React 19TypeScript

React 19 : ce qui change concrètement pour les développeurs

· 6 min de lecture

React 19 est la plus grosse release depuis les hooks. Pas parce qu’elle ajoute de la syntaxe tape-à-l’oeil, mais parce qu’elle supprime des catégories entières de code qu’on écrivait à la main. Le React Compiler tue la plupart de la mémoïsation manuelle. Les Actions remplacent les handlers onSubmit câblés à useState + useEffect. use() permet de lire des promises et du context n’importe où. Et forwardRef est mort, remplacé par ref en tant que simple prop. Chacun de ces changements pris isolément économise quelques lignes. Ensemble, ils redéfinissent la façon dont on écrit des composants React.

Le React Compiler : la mémoïsation automatique

Chaque développeur React s’est battu avec useMemo, useCallback et React.memo. Trop peu de wrapping et on subit des re-renders inutiles. Trop et on se retrouve avec du code illisible, plus dur à maintenir que le problème de performance qu’il résout. Le React Compiler élimine ce dilemme.

Il analyse les composants au build et insère automatiquement la mémoïsation qui compte. On écrit du JavaScript standard, et le compilateur détermine ce qui doit être mis en cache.

// Avant : mémoïsation manuelle partout
const ProductCard = React.memo(function ProductCard({ product, onAdd }: Props) {
  const formattedPrice = useMemo(() => formatCurrency(product.price), [product.price]);
  const handleClick = useCallback(() => onAdd(product.id), [onAdd, product.id]);

  return (
    <div>
      <h3>{product.name}</h3>
      <span>{formattedPrice}</span>
      <button onClick={handleClick}>Add to cart</button>
    </div>
  );
});
// Après : le compilateur s'en charge
function ProductCard({ product, onAdd }: Props) {
  const formattedPrice = formatCurrency(product.price);

  return (
    <div>
      <h3>{product.name}</h3>
      <span>{formattedPrice}</span>
      <button onClick={() => onAdd(product.id)}>Add to cart</button>
    </div>
  );
}

Même performance, moitié moins de code, zéro charge cognitive. Le compilateur comprend les règles de React (rendu pur, props immuables) et optimise en conséquence. Les arrow functions inline dans le JSX ne sont plus un problème de performance.

Actions et useActionState : les formulaires sans le boilerplate

La gestion de formulaires en React a toujours été verbeuse. Il faut un state pour les valeurs des inputs, un state pour l’indicateur de chargement, un state pour les erreurs, un handler async qui relie le tout, et des blocs try/catch autour de chaque soumission. Les Actions condensent tout ça en un seul pattern.

import { useActionState } from 'react';

async function loginAction(prevState: ActionState, formData: FormData) {
  const email = formData.get('email') as string;
  const password = formData.get('password') as string;

  try {
    await api.login({ email, password });
    return { success: true, error: null };
  } catch (e) {
    return { success: false, error: 'Invalid credentials' };
  }
}

function LoginForm() {
  const [state, action, isPending] = useActionState(loginAction, {
    success: false,
    error: null,
  });

  return (
    <form action={action}>
      <input name="email" type="email" required />
      <input name="password" type="password" required />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Connexion...' : 'Se connecter'}
      </button>
      {state.error ? <p>{state.error}</p> : null}
    </form>
  );
}

Pas de useState pour le chargement. Pas de onSubmit avec preventDefault. Pas de useEffect pour réinitialiser l’état d’erreur. La prop action sur <form> est maintenant un concept natif de React, et isPending suit le cycle de vie async automatiquement.

useOptimistic : du feedback UI instantané

useOptimistic se combine avec les Actions pour afficher un retour immédiat avant la réponse du serveur. La valeur optimiste se réinitialise automatiquement quand l’Action se termine.

import { useOptimistic } from 'react';

function LikeButton({ count, onLike }: Props) {
  const [optimisticCount, setOptimisticCount] = useOptimistic(count);

  async function handleLike() {
    setOptimisticCount(optimisticCount + 1);
    await onLike();
  }

  return (
    <form action={handleLike}>
      <button type="submit">{optimisticCount} likes</button>
    </form>
  );
}

Le compteur s’incrémente instantanément au clic. Si l’appel serveur échoue, React revient à la vraie valeur. Pas de logique de rollback manuelle, pas de snapshot d’état précédent.

Le hook use() : lire des promises et du context n’importe où

use() est différent de tous les autres hooks parce qu’il peut être appelé dans des conditions et des boucles. Il lit la valeur courante d’une promise ou d’un context, et fonctionne avec Suspense pour les données asynchrones.

import { use, Suspense } from 'react';

function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise);

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

function Page({ userId }: { userId: string }) {
  const userPromise = fetchUser(userId);

  return (
    <Suspense fallback={<Skeleton />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

La promise est créée dans le parent et passée en prop. use() suspend le composant jusqu’à sa résolution. Pas de useEffect, pas d’état de chargement, pas de booléen isLoading. La boundary Suspense gère le fallback.

Pour le context, use() remplace useContext avec en plus la possibilité de lire le context conditionnellement.

import { use } from 'react';

function ThemeText({ showTheme }: { showTheme: boolean }) {
  if (showTheme) {
    const theme = use(ThemeContext);
    return <span>{theme}</span>;
  }
  return null;
}

Avec useContext, ça lèverait une erreur parce que les hooks ne peuvent pas être appelés conditionnellement. use() n’a pas cette restriction.

ref en prop : forwardRef disparaît

forwardRef a toujours été bancal. Il enveloppait le composant dans une higher-order function, cassait l’inférence TypeScript de manière subtile, et rendait le composant plus difficile à lire. React 19 permet d’accepter ref comme une prop classique.

// Avant : le wrapper forwardRef
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
  return <input ref={ref} {...props} />;
});
// Après : ref est une simple prop
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} {...props} />;
}

Pas de wrapper, pas de gymnastique générique, pas d’indentation supplémentaire. La signature du composant dit tout. C’est particulièrement précieux dans les librairies de design system où chaque composant forward des refs.

Les métadonnées du document dans les composants

Les éléments title, meta et link peuvent maintenant être rendus directement dans les composants. React 19 les remonte automatiquement dans le <head>.

function BlogPost({ post }: { post: Post }) {
  return (
    <article>
      <title>{post.title}</title>
      <meta name="description" content={post.excerpt} />
      <meta property="og:title" content={post.title} />
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

Plus besoin de react-helmet ou d’une librairie de gestion du head séparée. Les métadonnées vivent à côté du contenu qu’elles décrivent, là où elles ont logiquement leur place.

Conclusion

React 19 supprime les frictions à tous les niveaux. Le Compiler élimine la corvée de mémoïsation. Les Actions et useActionState simplifient les formulaires à leur essence. use() rend la lecture de données asynchrones et de context conditionnel naturelle. L’abandon de forwardRef nettoie les signatures de composants dans des codebases entières. Ce ne sont pas des features expérimentales derrière des flags. Elles sont stables, livrées, et prêtes à réduire la quantité de code que vous maintenez dès aujourd’hui.