React 19.3 Au-delà des gros titres : submitter, Fullscreen Props et taille du bundle
Une présentation pratique des modifications mineures de React 19.3 : la propriété submitter, les props de l’événement fullscreen, les transitions parallèles, onReset, les correctifs importants de bogues et l’augmentation réelle de la taille.
Les notes de version mettent généralement en avant un petit nombre de fonctionnalités majeures, et React 19.3 ne fait pas exception. En dessous se trouve une liste des « autres changements notables » ainsi qu’une longue page de correctifs de bugs, la plupart étant des lignes simples que l’on peut facilement ignorer. Cependant, certaines de ces lignes modifient la manière dont vous écrivez des composants aujourd’hui, expliquent des bugs que vous avez peut-être déjà contournés, ou affectent ce que vous envoyez aux utilisateurs. Ce guide les examine approximativement dans l’ordre de leur importance potentielle pour vous, et se termine par une réponse mesurée à la question que tout le monde se pose après une mise à niveau : de combien le fichier d’installation a-t-il augmenté de taille ?
Déterminer quel bouton a soumis un formulaire
Un formulaire disposant de deux boutons de soumission, par exemple Enregistrer et Enregistrer & fermer, doit savoir lequel a été cliqué par l’utilisateur. Les navigateurs répondent depuis longtemps à cette question : l’événement natif SubmitEvent expose une propriété submitter qui indique le bouton ayant déclenché la soumission. Cependant, l’événement synthétique de React ne transmettait pas cette information. Les solutions habituelles consistaient à lire e.nativeEvent.submitter ou, de manière moins élégante, à enregistrer dans l’état le dernier bouton cliqué en espérant qu’aucune soumission par clavier ne passe inaperçue. React 19.3 ajoute la propriété submitter à l’événement de soumission synthétique (#35590).
Le moyen le plus simple de voir la différence est d’exécuter un formulaire avec les deux versions. La démonstration commence par ses imports ; l’import de nom d’espace est présent uniquement afin que le composant puisse afficher React.version.
import { useState } from 'react';
import * as React from 'react';
Le composant empêche la navigation par défaut, puis écrit la version React ainsi que la valeur de e.submitter dans l’état. Chaque bouton possède sa propre value, ce qui correspond exactement à ce sur quoi on effectuerait un branchement dans du code réel.
export default function App() {
const [msg, setMsg] = useState('press either button');
return (
<form onSubmit={e => {
e.preventDefault();
setMsg(`${React.version}: e.submitter → ${e.submitter ? e.submitter.value : String(e.submitter)}`);
}}>
<button value="save">Save</button>{' '}
<button value="save-close">Save & close</button>
<p className="msg">{msg}</p>
</form>
);
}
Une seule règle applique un style de police monospace à la ligne de sortie.
.msg { margin: 10px 0 0; font-family: ui-monospace, monospace; }
Le même formulaire sur 19.3
La version 19.3 de la démonstration est identique, c’est justement l’idée : seule la version React change. Les imports restent les mêmes.
import { useState } from 'react';
import * as React from 'react';
Le composant aussi.
export default function App() {
const [msg, setMsg] = useState('press either button');
return (
<form onSubmit={e => {
e.preventDefault();
setMsg(`${React.version}: e.submitter → ${e.submitter ? e.submitter.value : String(e.submitter)}`);
}}>
<button value="save">Save</button>{' '}
<button value="save-close">Save & close</button>
<p className="msg">{msg}</p>
</form>
);
}
Et le fichier de style également.
.msg { margin: 10px 0 0; font-family: ui-monospace, monospace; }
Au 19.2, le message affiche undefined, car l’événement synthétique ne possède pas ce champ. Au 19.3, il affiche save ou save-close, en fonction du bouton. Gardez à l’esprit quel chemin de code est concerné. Si vous soumettez via les actions du formulaire plutôt qu’avec un gestionnaire onSubmit, React lisait déjà l’élément de soumission lors de la création de FormData (#29028) ; par conséquent, la paire name et value du bouton pressé était déjà présente dans les données de l’action. Cette nouvelle propriété est importante pour les gestionnaires d’événements.
Remarque pratique : la valeur de l’élément de soumission n’est utile que si vous définissez réellement value (et généralement aussi name) pour chaque bouton. Un bouton sans ces attributs génère une chaîne vide, ce qui peut facilement être confondu avec un manque de fonctionnalité.
Événements en plein écran en tant que props de première classe
L’API Fullscreen envoie les événements fullscreenchange et fullscreenerror sur l’élément concerné. React ne reconnaissait pas ces noms d’événements, ce qui obligeait des composants tels que les lecteurs vidéo à ajouter manuellement des écouteurs à l’aide de addEventListener au sein d’un effet, en se souvenant de les supprimer lors du nettoyage. React 19.3 ajoute onFullscreenChange et onFullscreenError en tant que propriétés ordinaires (#34621).
La démonstration nécessite un ref pour l’élément qui passe en plein écran ainsi qu’un état pour le journal des événements.
import { useRef, useState } from 'react';
Remarquez que les deux gestionnaires se trouvent directement sur le div. Le gestionnaire de changement vérifie document.fullscreenElement pour déterminer si l’élément vient d’entrer ou de quitter le mode plein écran, car l’événement est déclenché dans les deux sens. Le bouton Passer en plein écran ajoute également un .catch à la promesse retournée par requestFullscreen(), car un refus est signalé à la fois comme une promesse rejetée et comme un événement d’erreur.
export default function App() {
const box = useRef(null);
const [log, setLog] = useState([]);
const add = line => setLog(l => [...l, line]); return (
<div
ref={box}
className="stage"
onFullscreenChange={() => add(document.fullscreenElement ? 'entered fullscreen' : 'left fullscreen')}
onFullscreenError={e => add('fullscreen error: ' + e.type)}
>
<button onClick={() => box.current.requestFullscreen().catch(err => add('rejected: ' + err.message))}>
Go fullscreen
</button>{' '}
<button onClick={() => document.fullscreenElement && document.exitFullscreen()}>Exit</button>
<pre className="log">{log.join('\n') || 'no events yet'}</pre>
</div>
);
}
Le fichier de style utilise la pseudo-classe :fullscreen pour centrer le contenu lorsque l’écran est entièrement occupé, et light-dark() pour choisir un arrière-plan adapté au thème de couleur.
.stage { padding: 12px; border-radius: 10px; background: light-dark(#f1f5f9, #262a33); }
.stage:fullscreen { display: grid; place-content: center; }
.log { margin: 10px 0 0; font-size: 14px; }
onFullscreenError est déclenché lorsque le navigateur refuse la demande. Les deux causes les plus fréquentes sont l’appel à requestFullscreen() en dehors d’un geste de l’utilisateur, et son exécution à l’intérieur d’un <iframe> qui ne possède pas l’attribut allow="fullscreen". Si vous insérez un lecteur sur une page de tiers et que le mode plein écran ne fonctionne pas, c’est d’abord cette autorisation manquante dans l’iframe à vérifier, et la nouvelle propriété vous offre un endroit adapté pour en enregistrer le message.
Les transitions ne s’attendent plus les unes les autres
D’après le journal des modifications, chaque transition est désormais affichée séparément au lieu d’être regroupée avec les autres en une seule représentation, de sorte qu’une transition lente ne peut pas bloquer des transitions non liées. Cette modification (#37290) active le paramètre enableParallelTransitions.
Au préalable à la version 19.3, si une deuxième mise à jour de startTransition arrivait tandis que React était encore en train d’afficher la première, les deux étaient fusionnées. Le rendu en cours était relancé avec les deux mises à jour appliquées, et aucune ne pouvait être confirmée tant que ce rendu combiné n’était pas terminé. Une transition rapide et peu coûteuse pouvait donc être bloquée par une transition coûteuse avec laquelle elle n’avait aucun rapport. Désormais, chaque transition constitue une unité de travail indépendante. Si vous souhaitez comprendre comment React traite ces unités en interne, notre article sur la manière dont React encode la priorité des mises à jour explique le modèle sous-jacent.
La manière d’écrire les transitions ne change pas, et les garanties de tri de React s’appliquent toujours : une mise à jour ultérieure ne sera jamais enregistrée en même temps qu’un état plus ancien. Cependant, une hypothèse devient fausse : les mises à jour générées par deux appels distincts à startTransition peuvent désormais être exécutées dans des frames différents. Si plusieurs mises à jour doivent apparaître ensemble, placez-les dans un seul appel à startTransition.
Ne vous attendez pas à voir cela dans un exemple simplifié. Une tentative de le reproduire avec un arbre lent composé de 300 composants et une mise à jour rapide et triviale programmée 50 ms plus tard a montré que la mise à jour rapide était enregistrée après la lente, tant sous 19.2 que sous 19.3. Cette amélioration se situe au niveau du planificateur et se manifeste dans les grandes applications sous charge réelle, et non dans de petits exemples.
Formulaires : onReset après une action, et nouvelles formulations pour les erreurs
Lorsque l’action d’un formulaire est terminée, React réinitialise automatiquement le formulaire. Jusqu’à présent, cette réinitialisation s’effectuait avec son système d’événements désactivé, ce qui signifiait que l’événement reset était ignoré. Le gestionnaire onReset, conçu pour effacer l’état contrôlé, ne s’exécutait jamais, et les champs contrôlés continuaient d’afficher leurs valeurs précédentes (#35176, qui corrige le problème #33630). Dans la version 19.3, onReset est également déclenché lors de la réinitialisation automatique. Si vous avez contourné ce bug en écoutant vous-même l’événement natif reset, vous pouvez revenir à cette approche et supprimer l’effet correspondant.
Un changement esthétique connexe : les messages d’erreur de useActionState font désormais référence à « état d’action » plutôt qu’à « état de formulaire » (#35790). Le comportement reste inchangé, mais les recherches dans les journaux, les règles d’alerte ou les tests qui correspondent à l’ancienne formulation doivent être mis à jour.
Changements en temps de développement : Mode strict, use() et mise à jour rapide
Trois changements ne concernent que les versions de développement, mais chacun peut révéler des surprises après une mise à jour.
- Mode strict pendant l’hydratation. Les effets sont maintenant invoqués deux fois lors de l’hydratation en mode strict (#35961), ce qui correspond au comportement des racines rendues côté client depuis React 18. Si une application rendue côté serveur semblait seulement sûre en mode strict parce que l’hydratation sautait la deuxième invocation, cela le mettra en évidence. La solution habituelle consiste à rendre les effets idempotents et à assurer leur nettoyage adéquat.
use(). Il est autorisé d’appeler use de manière conditionnelle. Le problème survient lorsqu’un composant est suspendu à cause de use(), puis, lors d’une nouvelle tentative, emprunte une branche qui ne lance jamais use(). La frontière Suspense se résout alors pour une raison erronée. React émet désormais un avertissement de développement indiquant qu’une bibliothèque a suspendu son exécution via use() lors d’un rendu précédent, a ignoré use() une fois ce dernier terminé, et que c’est là une utilisation incorrecte du hook. Le texte suggère que la cause est souvent une bibliothèque de données plutôt que votre propre composant (#37104, #37203, #37491).lazy(), une fonction de comparaison memo() modifiée, et une modification qui transforme un composant en un autre type de composant (#36965, #36964, #36963, #36950). Si ces modifications forçaient parfois un rechargement complet de la page, c’était en raison de ces causes.Changements mineurs dans le DOM
- Les mises à jour
resizesont désormais regroupées jusqu’au prochain frame (#35117), de la même manière que les événements de défilement. L’état défini dans un écouteurresizen’est plus mis à jour prématurément simplement parce qu’un clic survient au milieu d’un frame. maskTypeest désormais reconnu comme une propriété SVG (#35921).- Les ressources de module peuvent désormais comporter une indication de priorité : transmettez
fetchPriorityàReactDOM.preloadModuleouReactDOM.preinitModule, et React l’affichera en tant qu’attributfetchpriority(#36835). Cela permet à un plugin de bundler RSC de marquer les blocs de composants clients comme ayant une faible priorité. Les fonctions non modulairespreloadetpreinitle prenaient déjà en charge. credentiallessest désormais traité comme un attribut booléen sur<iframe>(#36148), et React suggère la forme correcte si vous écrivezcredentialLess(#36322).- React ne réassigne plus
innerHTMLlorsqu’il n’a pas changé (#36949). Une nouvelle rendu avecdangerouslySetInnerHTMLidentique ne fait plus en sorte que le navigateur analyse à nouveau le markup, ce qui permet également de conserver tout état DOM qu’il contient. defaultValuesur les champs de type"number"se comporte désormais de la même manière que pour les autres types de champs (#36980).- L’événement
togglecopie la propriétésourcepour les popovers (#37389).
Rendu serveur et composants serveurs
- Les erreurs générées sur le serveur parviennent désormais au client avec leur
cause(#35810), et unAggregateErrorconserve son tableauerrors(#36156). Les limites d’erreurs et le code de journalisation qui analysent la chaîne d’erreurs voient donc la même structure des deux côtés. - Les composants serveurs peuvent désormais afficher
<Activity>, car Flight le comprend (#34697). react-dom/serverne bloque plus sous Deno (#35235), et le point d’entrée de Bun inclut désormais toutes les API Node (#34193).
Réparations de bugs dont vous avez pu observer les symptômes
La liste complète est longue. Ce sont les entrées qui correspondent le plus probablement à un bug que vous avez déjà traité.
useDeferredValuepourrait rester bloqué sur une valeur obsolète (#36134). Une valeur différée qui ne mettait pas à jour régulièrement ses données était un bug de React, et non une erreur dans votre code.- Les changements de contexte n’atteignaient pas toujours les solutions de secours de Suspense (#36160) ni un sous-arbre situé derrière une limite qui restait en mode suspension (#35839).
useEffectEventà l’intérieur des composantsforwardRefetmemolit des valeurs obsolètes (#34831).- Lorsqu’un arbre
<Activity>était caché,useSyncExternalStoremanquait les modifications du stockage (#36947). De plus, un arbre caché empêche l’élévation de son<title>(#34983), retient ses erreurs hors de l’interface visible (#35074) et ne montre plus ce qu’il rend dans les portails (#35091).
nonce (#37030).<ViewTransition> ont été résolus : l’un dans Mobile Safari (#35337) et l’autre lorsqu’il était utilisé avec SuspenseList (#35520). Cela est important si vous avez testé les versions expérimentales.decodeReplyFromBusboy supprimait les éléments FormData (#36468), les chaînes asynchrones profondes pouvaient faire déborder la pile d’exécution (#35612), et les informations de débogage pouvaient croître exponentiellement jusqu’à provoquer une erreur RangeError (#37481).Tout le ensemble de demandes de fusion est accessible via le CHANGelog de React.
Le coût en octets de cette mise à niveau
Peu après la publication, un commentaire dans la discussion r/reactjs a signalé une augmentation de la taille de react-dom-client.production.js de 523,5 kB à 610,5 kB et a demandé la raison de ce phénomène. En comparant les versions 19.2.0 et 19.3.0 publiées sur unpkg, on obtient les chiffres suivants :
cjs/react-dom-client.production.js, en format brut : 536 016 octets dans la version 19.2.0 contre 625 168 octets dans la version 19.3.0, soit une augmentation de 89 kB (16,6 %).- Le même fichier compressé avec gzip au niveau 9 : 94,7 kB contre 110,5 kB, soit une augmentation de 15,7 kB.
cjs/react-dom-server.node.production.js, en format brut : 277 046 octets contre 300 544 octets, soit une augmentation de 23,5 kB.cjs/react.production.js, en format brut : 17 217 octets contre 18 040 octets, soit une augmentation de 0,8 kB.
Les chiffres dans les commentaires de Reddit correspondent à des tailles exprimées en KiB. La cause est simple : les effets de transition d’affichage, les références de fragments, browser() ainsi que le chemin de code des types fiables étaient auparavant masqués par des paramètres fonctionnels qui les retiraient de la version stable. Dans la version 19.3, ils sont inclus dans la version stable. Sur le réseau, la différence est d’environ 16 kB après compression, ce qui équivaut à un ensemble d’icônes de taille moyenne.
C’est un coût réel qu’il convient de prendre en compte dans votre budget de performance, mais ce n’est pas une régression dans le comportement de React. Si ces octets supplémentaires ont de l’importance pour votre application, les solutions les plus efficaces consistent généralement en le splitage du code, en réduisant vos propres dépendances et en différant le chargement des composants clients non essentiels, plutôt que de retarder la mise à jour.
Points clés
- Lisez directement
e.submitterdans les gestionnairesonSubmit; les actions de formulaire ont déjà reçu lenameet levaluedu bouton. - Remplacez les écouteurs manuels
fullscreenchangeparonFullscreenChangeetonFullscreenError, et gérez également la promesse rejetée parrequestFullscreen(). - Les appels distincts à
startTransitionpeuvent désormais s’exécuter dans des frames différents, il convient donc de regrouper les mises à jour qui doivent apparaître ensemble. onResets’exécute enfin après le réinitialisation automatique d’une action, et le mode strict appelle désormais les effets deux fois pendant l’hydratation, il faut donc vérifier à nouveau le nettoyage des effets.- Divers bugs de longue date, y compris un
useDeferredValuebloqué et des valeursuseEffectEventobsolètes, ont été corrigés ; vérifiez si des solutions de contournement dans votre code peuvent être supprimées.
react-dom côté client ; les fonctionnalités auparavant marquées comme optionnelles sont désormais incluses dans les versions stables.