12k
All articles

5 Choses Pour Lesquelles Vous N'avez Pas Besoin de React

Cinq API natives du navigateur remplacent des composants React courants : dialog, Popover, Custom Elements, container queries et View Transitions.

OpenReplay Team
OpenReplay Team
5 Choses Pour Lesquelles Vous N'avez Pas Besoin de React

La plateforme navigateur intègre désormais des remplaçants natifs, disponibles en standard, pour plusieurs primitives d’interface utilisateur qui nécessitaient autrefois des composants React ou des bibliothèques tierces : les boîtes de dialogue modales, les popovers et infobulles, les widgets réutilisables indépendants du framework, les mises en page responsives adaptées au conteneur, et les transitions animées entre vues. Cet article n’est pas un plaidoyer contre React — il reste l’outil approprié pour la gestion d’état partagé complexe, les workflows de formulaires étendus, et les écosystèmes comme Next.js et Remix. Il s’agit d’une liste de contrôle pour les mainteneurs : cinq catégories de composants que vous avez peut-être déjà dans une base de code React, et que le navigateur peut désormais gérer nativement, sans aucun poids supplémentaire dans le bundle JavaScript.

Ce contenu s’adresse au développeur React en activité dont le modèle mental de « ce que le navigateur sait faire » s’est figé quelque part autour de 2020. React 19 est la version stable actuelle, et plusieurs des patterns que son écosystème avait résolus font maintenant partie de la plateforme. Chaque section ci-dessous identifie le réflexe React concerné, l’API native qui le remplace, et la mise en garde d’accessibilité spécifique à connaître avant de supprimer du code.

Points Clés

  • L’élément HTML <dialog> utilisé avec showModal() fournit nativement le piégeage du focus, la fermeture par la touche Échap, et un pseudo-élément ::backdrop — éliminant la plupart des raisons de dépendre d’une bibliothèque modale React.
  • L’API Popover affiche les éléments dans la couche supérieure du navigateur, supprimant toute une classe de bugs liés aux z-index et au découpage par overflow: hidden qui affectent les infobulles et menus déroulants React faits maison.
  • Les Custom Elements avec Shadow DOM vous permettent de distribuer un seul widget fonctionnant dans n’importe quel framework ou en HTML brut, sans avoir à le réimplémenter pour chaque stack.
  • Les CSS Container Queries (@container) permettent à un composant de réagir à la largeur de son parent, remplaçant les hooks ResizeObserver et l’état React utilisés uniquement pour des décisions de mise en page.
  • L’API View Transitions (document.startViewTransition()) anime nativement les changements d’état du DOM, couvrant de nombreux cas d’usage précédemment gérés par Framer Motion ou react-transition-group.

Modales : utilisez l’élément <dialog> plutôt qu’une bibliothèque modale

Pour les boîtes de dialogue modales, l’élément HTML natif <dialog> appelé via showModal() vous offre le piégeage du focus, le contenu d’arrière-plan rendu inerte, la fermeture par la touche Échap, et le style du backdrop — des comportements qu’une modale React personnalisée doit implémenter manuellement et implémente souvent incorrectement. L’élément <dialog> fait partie de Baseline ; vérifiez la date exacte de disponibilité sur MDN avant de publier une documentation interne.

Le pattern React. Les équipes ont généralement recours à react-modal, Radix Dialog, ou un hook useModal personnalisé s’appuyant sur un portail. Le pattern avec hook combine généralement createPortal, un useEffect qui bascule document.body.style.overflow, et un piège à focus écrit manuellement. Les enregistrements de sessions en production de ces implémentations font fréquemment apparaître des utilisateurs qui tabbent hors de la modale vers le contenu en arrière-plan — symptôme d’une logique de piège à focus incomplète.

L’API native.

<dialog id="confirm" aria-labelledby="confirm-title">
  <h2 id="confirm-title">Delete project?</h2>
  <p>This action cannot be undone.</p>
  <form method="dialog">
    <button value="cancel">Cancel</button>
    <button value="confirm">Delete</button>
  </form>
</dialog>

<script>
  document.getElementById('confirm').showModal();
</script>

showModal() place la boîte de dialogue dans la couche supérieure, piège le focus à l’intérieur, rend le reste du document inerte, et affiche le pseudo-élément ::backdrop que vous pouvez styliser en CSS. Un <form method="dialog"> ferme la boîte de dialogue et retourne la valeur value du bouton cliqué via dialog.returnValue — aucun écouteur d’événement n’est nécessaire.

Mises en garde. Le piège d’accessibilité est que <dialog> n’annonce pas automatiquement un label. Vous avez besoin d’un aria-labelledby pointant vers un titre visible (ou d’un aria-label) pour que les lecteurs d’écran puissent l’identifier. Si la boîte de dialogue est non-modale — ouverte avec show() plutôt que showModal() — elle ne piège pas le focus, et vous préférerez peut-être l’API Popover. React ou une bibliothèque reste le meilleur choix lorsque vous avez besoin d’une logique d’ouverture/fermeture déclarative liée à l’état et étroitement couplée à d’autres composants, ou d’une animation s’exécutant avant le démontage de la boîte de dialogue.

Popovers, infobulles et menus déroulants : utilisez l’API Popover

L’API Popover affiche les éléments dans la couche supérieure du navigateur, ce qui signifie qu’un popover apparaît toujours au-dessus des autres contenus, quel que soit le contexte d’empilement ou la présence d’overflow: hidden sur un ancêtre. Cela élimine toute la catégorie de conflits de z-index et de bugs de découpage que produisent les implémentations d’infobulles et de menus déroulants faites maison.

Le pattern React. Floating UI, Radix Popover, et les primitives d’overlay de React-Aria sont des dépendances courantes. Elles gèrent le positionnement, la fermeture au clic extérieur, et le rendu via portail. Pour une simple infobulle, c’est beaucoup de code à importer.

L’API native.

<button popovertarget="menu">Open menu</button>

<div id="menu" popover>
  <a href="/account">Account</a>
  <a href="/logout">Log out</a>
</div>

L’attribut popover seul — sans JavaScript — vous donne un élément qui se bascule via un bouton popovertarget, se ferme au clic extérieur et à la touche Échap, et s’affiche dans la couche supérieure. La valeur par défaut popover="auto" active la fermeture légère (light-dismiss) ; popover="manual" la désactive pour les cas où vous souhaitez un contrôle explicite. L’API Popover est Baseline Newly Available ; consultez le tableau de compatibilité MDN pour connaître son statut actuel.

Mises en garde. Le piège d’accessibilité est que, contrairement à showModal() de <dialog>, l’API Popover ne gère pas automatiquement le focus. Si votre popover est fonctionnellement un menu, vous devez tout de même appliquer role="menu", gérer le tabindex itinérant (roving tabindex), et déplacer le focus dans le popover à son ouverture. Pour le positionnement relatif au déclencheur, vous avez également besoin du CSS Anchor Positioning, dont le statut Baseline est plus limité — vérifiez sur MDN avant de vous y fier en cross-browser. Pour les menus complexes avec sous-menus, patterns de navigation au clavier et typeahead, une bibliothèque comme Radix ou React-Aria représente encore un gain de travail réel.

Widgets réutilisables : utilisez les Custom Elements et le Shadow DOM

Un Custom Element enregistré avec customElements.define() fonctionne dans n’importe quel contexte HTML — React, Vue, Angular, Svelte, ou un fichier HTML brut — sans réimplémentation. Combiné au Shadow DOM, il offre l’encapsulation des styles sans CSS Modules, CSS-in-JS, ni étape de build. Les Custom Elements et le Shadow DOM sont Baseline Widely Available ; vérifiez l’année sur MDN.

Les Web Components n’ont pas remplacé React dans le développement d’applications grand public. Ce qu’ils ont remplacé, c’est la nécessité de distribuer le même widget cinq fois — une fois par framework — lorsque vous maintenez un design system ou distribuez un embed tiers.

Le pattern React. Un bouton, un badge, ou un graphique réutilisable encapsulé dans un composant React, publié sur npm, et réimplémenté (ou ré-encapsulé) pour chaque équipe utilisant un framework différent.

L’API native.

class CopyButton extends HTMLElement {
  connectedCallback() {
    this.attachShadow({ mode: 'open' }).innerHTML = `
      <style>button { padding: 6px 12px; }</style>
      <button><slot>Copy</slot></button>
    `;
    this.shadowRoot.querySelector('button')
      .addEventListener('click', () => {
        navigator.clipboard.writeText(this.dataset.value ?? '');
      });
  }
}
customElements.define('copy-button', CopyButton);

Utilisé comme <copy-button data-value="hello">Copy</copy-button> dans n’importe quel HTML, y compris dans du JSX. React 19 prend en charge les custom elements directement, notamment le passage de props de type objet et l’écoute d’événements personnalisés.

Mises en garde. Le piège d’accessibilité est que l’arbre d’accessibilité ne traverse pas les frontières du shadow DOM par défaut — les références aria-labelledby et aria-describedby dans le light DOM ne peuvent pas cibler des IDs à l’intérieur d’un shadow root, et vice versa. La spécification ARIA in HTML et la proposition reference target en cours traitent ce problème, mais les patterns pratiques actuels exigent soit des attributs ARIA explicites sur l’élément hôte, soit attachInternals() avec ElementInternals. React reste le meilleur choix lorsqu’un widget doit s’intégrer étroitement avec l’état applicatif, partager un React Context, ou utiliser Suspense.

Mise en page responsive au niveau du composant : utilisez les CSS Container Queries

Les CSS Container Queries (@container) permettent à un composant d’adapter sa mise en page en fonction de la largeur de son propre parent plutôt que du viewport. Cela élimine le pattern de hook useResizeObserver où l’état React suit les dimensions du conteneur uniquement pour piloter un className. Les Container Queries sont Baseline Widely Available — vérifiez l’année sur MDN.

Le pattern React. Un hook useResizeObserver (souvent issu de @react-hook/resize-observer ou écrit manuellement) connecté à l’état du composant qui permute une prop layout="compact" ou un className. Chaque redimensionnement déclenche un rendu React, même si le seul consommateur est le CSS.

L’API native.

.card-container {
  container-type: inline-size;
}

.card {
  display: grid;
  grid-template-columns: 1fr;
}

@container (min-width: 400px) {
  .card {
    grid-template-columns: 120px 1fr;
  }
}

Déclarez container-type: inline-size sur le parent, puis écrivez des règles @container pour l’enfant. Le navigateur gère nativement l’observation du redimensionnement. Pas de JavaScript, pas de re-rendus, pas de désaccord d’hydratation.

Le sélecteur :has() complète cette approche pour le stylage conditionnel basé sur l’état. Une règle comme form:has(input:invalid) button[type="submit"] { opacity: 0.5 } exprime ce qui nécessitait auparavant un useState et un pattern d’input contrôlé. :has() est Baseline Widely Available — vérifiez sur MDN.

Mises en garde. La considération d’accessibilité est subtile mais réelle : les container queries peuvent modifier radicalement la mise en page sans changer l’ordre du DOM, ce qui est favorable aux lecteurs d’écran, mais signifie que vous devez tout de même vérifier que l’ordre de lecture correspond à l’ordre visuel à chaque breakpoint. Les container queries introduisent également un comportement de containment qui peut affecter la disposition et le positionnement des éléments descendants — testez donc les composants qui s’appuient sur un positionnement relatif au viewport ou d’autres hypothèses de mise en page. L’état React reste justifié lorsque la décision de mise en page pilote plus que le style — par exemple, lorsque vous devez afficher un arbre de composants différent, et pas seulement restyliser un existant.

Transitions animées : utilisez l’API View Transitions

L’API View Transitions enveloppe une mise à jour du DOM dans une animation de fondu enchaîné par défaut, avec un contrôle CSS complet sur la transition via les pseudo-éléments ::view-transition-*. Pour les transitions dans le même document, elle couvre la majorité des animations de transition de routes et d’états qui nécessitaient auparavant des bibliothèques d’animation.

Le pattern React. Framer Motion, react-transition-group, ou des wrappers AnimatePresence autour des composants de route. Ces solutions fonctionnent, mais elles exigent que l’animation soit exprimable dans le modèle de rendu de React, ce qui est maladroit pour les transitions qui couvrent le démontage d’un arbre et le montage d’un autre.

L’API native.

function navigate(url) {
  if (!document.startViewTransition) {
    updateDOM(url);
    return;
  }
  document.startViewTransition(() => updateDOM(url));
}

document.startViewTransition() prend un callback qui effectue la mise à jour du DOM. Le navigateur capture l’état avant, exécute le callback, capture l’état après, et effectue un fondu enchaîné entre les deux. Pour animer un élément spécifique à travers la transition — par exemple, une miniature qui s’agrandit en vue détaillée — donnez aux éléments correspondants le même view-transition-name en CSS. Les View Transitions dans le même document sont Baseline Newly Available ; les View Transitions cross-document (pour les navigations MPA) ont un support plus limité — consultez le tableau de compatibilité MDN et le blog WebKit pour le statut actuel de Safari avant de vous fier au mode cross-document.

Mises en garde. Le piège d’accessibilité concerne le mouvement : respectez prefers-reduced-motion en encapsulant la transition dans une media query ou en ignorant complètement l’appel pour les utilisateurs qui ont opté pour cette préférence. Le fondu enchaîné par défaut est bref, mais reste une animation. Les bibliothèques React restent le meilleur choix lorsque vous avez besoin de physique de ressort (spring physics), de transitions pilotées par des gestes, ou d’animations pouvant s’interrompre et s’inverser en cours d’exécution — les view transitions sont atomiques et ne sont pas conçues pour cela.

Là où React reste supérieur

Les cinq substitutions ci-dessus ciblent des catégories de composants spécifiques. Pour tout ce qui suit, React reste le bon outil, et le remplacer par des fonctionnalités de la plateforme coûterait plus que cela n’apporterait.

  • État partagé complexe entre des composants distants. Lorsque plusieurs parties non liées de l’interface s’abonnent au même état évolutif avec des sélecteurs dérivés, des bibliothèques comme Zustand, Jotai, ou Redux Toolkit accomplissent un travail que la plateforme ne fait pas. Les événements personnalisés sur les Web Components peuvent transporter des données, mais ne modélisent pas l’état dérivé.
  • Workflows de formulaires étendus avec validation inter-champs et rendu dynamique. Le <form> natif, l’API Constraint Validation, et FormData gèrent proprement la soumission d’un formulaire simple. Les assistants multi-étapes, les champs conditionnels dépendant de valeurs situées ailleurs dans le formulaire, la validation côté serveur fusionnée avec la validation côté client, et les tableaux de champs bénéficient encore de React Hook Form ou TanStack Form.
  • Rendu piloté par le serveur et récupération de données. Les React Server Components, le hook use() pour les données asynchrones, et le modèle SSR en streaming de Next.js et Remix résolvent les problèmes de coordination d’hydratation, de code-splitting et de récupération de données que la plateforme n’adresse pas directement.
  • Maturité de l’écosystème pour le routing et les couches de données. TanStack Router, TanStack Query, et l’écosystème React Router établi fournissent l’invalidation de cache, les mises à jour optimistes, et les patterns de route-loader qu’il faudrait un travail considérable pour reproduire avec des APIs natives.
  • Conventions d’équipe et investissement existant. Une base de code, un pipeline de recrutement, un design system et une CI construits autour de React constituent en eux-mêmes un actif. La démarche d’audit présentée ici consiste à supprimer des composants spécifiques là où la plateforme suffit désormais — pas à migrer des stacks entières.

L’action concrète : ouvrez votre répertoire de composants React le plus volumineux, recherchez Modal, Popover, Tooltip, Dropdown, et tout import useResizeObserver. Chacun est un candidat au remplacement natif décrit ci-dessus. Vérifiez le statut Baseline de l’API sur MDN pour votre plage de navigateurs supportés, déployez le remplacement derrière un feature flag, et mesurez le delta du bundle. Le navigateur a rattrapé son retard — le travail restant consiste à auditer les dépendances dont vous n’avez plus besoin.

FAQ

Puis-je utiliser l'élément dialog natif avec le modèle d'état de React ?

Oui. Attachez une ref à l'élément dialog et appelez ref.current.showModal() ou ref.current.close() depuis des effets pilotés par l'état React. La boîte de dialogue reste dans l'arbre React et accepte normalement des enfants JSX, mais vous contournez les props d'ouverture pilotées par useState sur la sortie rendue. La principale friction est que React ne ré-exécute pas les effets pour l'événement cancel interne de la boîte de dialogue — attachez donc un écouteur close natif via useEffect pour resynchroniser l'état.

Comment les Custom Elements transmettent-ils des données complexes à React, et vice versa ?

React 19 passe les props non-string directement aux propriétés des custom elements plutôt que de les sérialiser en attributs, donc les objets et tableaux fonctionnent sans encodage JSON. Les custom elements renvoient des données via CustomEvent, que React 19 écoute en utilisant des props de gestionnaire préfixées par on (par exemple, onMyEvent). Avec React 18 et les versions antérieures, vous devez attacher les écouteurs d'événements de manière impérative via une ref, car les événements synthétiques ne gèrent pas les noms d'événements personnalisés.

Les container queries et le sélecteur :has() nuisent-ils aux performances de rendu ?

Les deux ont un coût mesurable, mais sont généralement moins coûteux que les alternatives JavaScript qu'ils remplacent. Les container queries exigent que le navigateur maintienne un contexte de containment et réévalue les règles correspondantes lors des changements de taille, ce qui reste plus rapide qu'un callback ResizeObserver déclenchant un rendu React. Le sélecteur :has() peut être coûteux lorsqu'il est utilisé avec des sélecteurs de sujet larges sur de grands arbres DOM ; limitez-le à des parents spécifiques plutôt que de l'appliquer à body ou aux éléments racine.

L'API View Transitions fonctionne-t-elle avec les routeurs côté client comme React Router ?

Oui, pour les transitions dans le même document. Encapsulez le callback de navigation de votre routeur dans document.startViewTransition() afin que la mise à jour du DOM effectuée par React lors du changement de route s'exécute à l'intérieur de la transition. React Router v6 et TanStack Router supportent tous deux ce pattern via l'interception de navigation. Les view transitions cross-document, qui animent les chargements de pages complètes, nécessitent un opt-in supplémentaire via la règle CSS @view-transition et bénéficient d'un support navigateur plus restreint — vérifiez sur MDN avant de vous y fier.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

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