12k
All articles

Comment utiliser les refs de Fragment dans React 19.3

Utilisez les refs de Fragment de React 19.3 pour gérer événements, focus, observateurs et mesures sur des éléments frères sans wrapper. Découvrez leurs limites et quand garder un élément DOM.

OpenReplay Team
OpenReplay Team
Comment utiliser les refs de Fragment dans React 19.3

Dans React 19.3, passer une ref à <Fragment> vous donne un FragmentInstance : un objet dont les méthodes DOM agissent sur l’ensemble des enfants du Fragment comme un groupe, sans ajouter d’élément à la page.

Il vous est sans doute déjà arrivé d’envelopper un groupe d’éléments frères dans une <div> uniquement pour pouvoir leur attacher une ref, puis de voir une grille CSS ou une rangée flex se casser parce que les cartes n’étaient plus des enfants directs du conteneur. Les refs de Fragment vous permettent d’attacher des écouteurs d’événements, des observers et une gestion du focus à des groupes d’éléments frères, sans modifier la structure du DOM. Ce guide ne reprend pas l’intégralité des nouveautés de la version (l’article what’s new in React 19 couvre les fonctionnalités précédentes). Il explique le fonctionnement de l’API, quels enfants chaque méthode peut atteindre et dans quels cas un élément wrapper reste le meilleur choix.

Points clés

  • Les refs de Fragment sont stables dans React 19.3. La syntaxe abrégée <></> ne peut pas recevoir de ref : vous devez donc importer Fragment et écrire <Fragment ref={ref}>.
  • La ref contient un FragmentInstance, ni un nœud DOM ni un tableau, qui expose douze méthodes réparties en quatre groupes : événements, focus, observers, et mesure/défilement.
  • Les méthodes d’événements, d’observation et de mesure n’atteignent que les enfants DOM de premier niveau du Fragment. focus() et focusLast() parcourent les éléments imbriqués en profondeur (depth-first).
  • scrollIntoView() sur un FragmentInstance n’accepte que le booléen alignToTop et lève une exception si vous lui passez un objet d’options.

Quel est le problème de la div wrapper ?

Avant React 19.3, le seul moyen d’agir sur un groupe d’éléments frères consistait à les envelopper dans un véritable élément DOM et à placer la ref sur cet élément. Or ce wrapper modifie l’arbre de mise en page, et les systèmes de layout comme CSS grid et flexbox ne prennent en compte que les enfants directs.

// Before: the div is now the only grid item
<div className="grid">
  <div ref={groupRef}><Card /><Card /><Card /></div>
</div>

// After: each card is still a grid item
<div className="grid">
  <Fragment ref={groupRef}><Card /><Card /><Card /></Fragment>
</div>

Dans la version avec la div wrapper, les cartes deviennent des enfants du wrapper, et non de la grille. La version avec Fragment laisse le DOM exactement tel qu’il serait sans aucune ref.

Comment fonctionne une ref de Fragment React ?

Une ref de Fragment React fonctionne comme n’importe quelle autre ref, à ceci près que la valeur assignée par React est un FragmentInstance. Les refs de Fragment sont une fonctionnalité stable de la version React 19.3, publiée le 9 septembre 2026. Vous avez besoin de React 19.3 ou d’une version ultérieure (react et react-dom 19.3.0+, y compris le correctif 19.3.1) : installez donc des versions 19.3.x identiques pour les deux paquets. Si vous migrez depuis une version majeure plus ancienne, consultez d’abord les erreurs courantes lors de la migration vers React 19.

La référence de <Fragment> liste douze méthodes :

Groupe de méthodesMéthodesPortéeRemarques
ÉvénementsaddEventListener, removeEventListener, dispatchEventEnfants de premier niveaudispatchEvent se déclenche sur le Fragment et peut remonter jusqu’à son parent DOM
Focusfocus, focusLast, blurÉléments imbriqués, en profondeurfocus() accepte des FocusOptions
ObserversobserveUsing, unobserveUsingEnfants de premier niveauIntersectionObserver ou ResizeObserver ; les nœuds texte ne sont pas pris en charge
Mesure/défilementgetClientRects, getRootNode, compareDocumentPosition, scrollIntoViewEnfants de premier niveaugetRootNode part du parent ; scrollIntoView n’accepte qu’un booléen

La plupart des surprises liées à cette API viennent de la règle du premier niveau. Un court exemple imbriqué montre précisément ce que chaque groupe de méthodes peut atteindre :

<Fragment ref={groupRef}>
  <div className="row">          {/* first-level: gets listeners, observed, measured */}
    <button>Save</button>        {/* nested: not observed; focus() can reach it */}
  </div>
  <Card />                       {/* component: the element Card renders at its top level is first-level */}
  Some text                      {/* text node: observeUsing does not work on it */}
</Fragment>

Les composants ne comptent pas comme un niveau. Ce qui compte, c’est l’élément DOM que chaque composant rend à son niveau supérieur : <Card /> est donc considéré comme un enfant de premier niveau, même s’il s’agit d’un composant React.

Comment ajouter un même écouteur d’événement à chaque enfant ?

Appeler addEventListener sur un FragmentInstance enregistre le gestionnaire sur tous les enfants DOM de premier niveau en une seule fois. Enregistrez-le dans un effet et retirez-le dans la fonction de nettoyage, en passant la même référence de fonction les deux fois.

import { Fragment, useEffect, useRef } from 'react';

function ClickGroup({ onClick, children }) {
  const groupRef = useRef(null);

  useEffect(() => {
    const group = groupRef.current;
    if (!group) return;
    group.addEventListener('click', onClick);
    return () => group.removeEventListener('click', onClick);
  }, [onClick]);

  return <Fragment ref={groupRef}>{children}</Fragment>;
}

La référence précise que React ajoute ou retire l’écouteur à mesure que des enfants entrent dans le Fragment ou en sortent. Passez un onClick stable (par exemple enveloppé dans useCallback), car une nouvelle fonction à chaque rendu oblige l’effet à se désabonner puis à se réabonner à chaque fois.

Les éléments imbriqués déclenchent tout de même l’écouteur lorsque l’événement remonte. Un clic sur un <button> situé dans une <div> de premier niveau remonte jusqu’à cette <div>, où le gestionnaire est attaché. Les événements qui ne remontent pas, comme focus ou mouseenter, n’atteindront pas l’écouteur depuis des éléments plus profonds.

Comment observer le groupe avec observeUsing ?

observeUsing() connecte un IntersectionObserver ou un ResizeObserver déjà créé à chaque enfant DOM de premier niveau. unobserveUsing() déconnecte cette même instance d’observer de ces enfants.

import { Fragment, useEffect, useRef } from 'react';

function VisibilityGroup({ onVisibilityChange, children }) {
  const groupRef = useRef(null);

  useEffect(() => {
    const group = groupRef.current;
    if (!group) return;
    const visible = new Set();
    const observer = new IntersectionObserver((entries) => {
      for (const entry of entries) {
        if (entry.isIntersecting) visible.add(entry.target);
        else visible.delete(entry.target);
      }
      onVisibilityChange(visible.size > 0);
    });
    group.observeUsing(observer);
    return () => {
      group.unobserveUsing(observer);
      observer.disconnect();
    };
  }, [onVisibilityChange]);

  return <Fragment ref={groupRef}>{children}</Fragment>;
}

Le Set suit les enfants actuellement en intersection : le groupe est ainsi considéré comme visible tant qu’au moins une carte est à l’écran. Les enfants peuvent être des composants qui n’acceptent jamais de ref. Comme l’indique la référence de Fragment, les nœuds texte ne sont pas observés, et React affiche un avertissement en mode développement lorsqu’un Fragment ne contient que du texte.

Déplacer le focus avec focus()

Appeler focus() sur une ref de Fragment déplace le focus vers le premier élément focalisable du groupe. Cela fonctionne même lorsque le champ se trouve à l’intérieur d’un <label> ou d’un <fieldset> rendu par un composant enfant qui n’expose pas de ref, car les méthodes de focus parcourent les enfants imbriqués en profondeur.

import { Fragment, useEffect, useRef, useState } from 'react';

function EditAddress() {
  const [editing, setEditing] = useState(false);
  const fieldsRef = useRef(null);

  useEffect(() => {
    if (editing) fieldsRef.current?.focus();
  }, [editing]);

  return (
    <section>
      <button onClick={() => setEditing(true)}>Edit address</button>
      {editing && (
        <Fragment ref={fieldsRef}>
          <AddressFields />
        </Fragment>
      )}
    </section>
  );
}

Un replay de session d’un formulaire ou d’une modale peut révéler que le focus n’atterrit nulle part après un changement d’état, si bien que la frappe suivante est envoyée à <body>. Appeler focus() sur une ref de Fragment corrige ce problème sans wrapper. Utilisez focusLast() pour cibler le dernier élément focalisable, et passez si nécessaire des FocusOptions telles que { preventScroll: true }. Une ref callback fonctionne également, mais veillez à ce qu’elle reste stable pour éviter qu’elle ne soit réexécutée à chaque rendu.

Quand un élément wrapper reste-t-il préférable ?

Utilisez un élément wrapper lorsque c’est le nœud DOM lui-même dont vous avez besoin. Une ref de Fragment agit sur un groupe d’enfants ; elle ne crée pas de boîte. Conservez le wrapper lorsque :

  • Vous devez styliser le groupe. Arrière-plans, bordures, padding et zones de grille nécessitent tous un véritable élément.
  • Vous avez besoin d’une boîte englobante unique. getClientRects() renvoie un tableau plat de rectangles pour les enfants, et non un rectangle unique combiné.
  • Vos cibles se trouvent plus profondément que les enfants de premier niveau. Les observers et les événements qui ne remontent pas n’atteignent jamais ces éléments.

N’oubliez pas non plus que scrollIntoView() n’accepte que alignToTop. Si vous avez besoin d’un défilement fluide ou d’un alignement block, utilisez la méthode de l’élément sur un véritable nœud.

Conclusion

Les refs de Fragment vous permettent d’attacher des écouteurs, des observers et une gestion du focus à des groupes d’éléments frères sans casser les layouts grid ou flex. Avant d’en utiliser une, vérifiez la règle du premier niveau : les événements, les observers et les mesures s’arrêtent aux enfants DOM directs, tandis que le focus va plus en profondeur. Pour l’essayer, passez à React 19.3 et remplacez une div wrapper servant uniquement à porter une ref par <Fragment ref>. Cette même version a également ajouté la prise en charge de Trusted Types, décrite dans l’annonce de la version React 19.3.

FAQ

Comment typer une ref de Fragment en TypeScript ?

Utilisez useRef avec le type FragmentInstance exporté par react et une valeur initiale à null. Dans @types/react 19.3, l'interface FragmentInstance est vide par défaut. @types/react-dom l'enrichit via declare module 'react' pour y ajouter focus, observeUsing, addEventListener et les autres méthodes. Si ces méthodes semblent manquer, installez @types/react-dom et assurez-vous que le module react-dom fait bien partie du graphe de types de votre projet.

Vers quel enfant scrollIntoView fait-il défiler une ref de Fragment ?

Si alignToTop vaut true ou est omis, scrollIntoView() aligne le premier enfant du Fragment avec le haut de l'ancêtre défilable. Si alignToTop vaut false, il aligne le dernier enfant avec le bas. Un Fragment vide se rabat sur l'élément frère le plus proche ou sur le parent. Passer un objet d'options, par exemple avec behavior défini à 'smooth', lève une erreur au lieu de faire défiler.

Les écouteurs d'événements d'une ref de Fragment fonctionnent-ils à l'intérieur d'une frontière Activity masquée ?

Non. React n'applique pas les écouteurs ajoutés via la méthode addEventListener d'un FragmentInstance aux enfants situés dans une frontière Activity masquée. Lorsque la frontière passe de l'état masqué à l'état visible, React applique automatiquement ces écouteurs. Votre code n'a pas besoin de les enregistrer à nouveau une fois le contenu rendu visible.

Que fait blur() sur une ref de Fragment si le focus se trouve en dehors du groupe ?

Rien. blur() sur un FragmentInstance retire le focus de document.activeElement uniquement lorsque cet élément se trouve à l'intérieur du Fragment. Si l'élément actif se trouve ailleurs dans la page, l'appel n'a aucun effet. Vous pouvez donc appeler blur() dans un gestionnaire de fermeture ou d'annulation sans vérifier au préalable où se trouve le focus.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.