Un guide complet sur l'API createPortal de React, couvrant les techniques de création de portails, les stratégies de gestion d'événements et les cas d'usage avancés.
React createPortal : Maîtriser la création de portails et la gestion des événements
Dans le développement web moderne avec React, il est crucial de créer des interfaces utilisateur qui s'intègrent de manière transparente à la structure du document sous-jacent. Bien que le modèle de composants de React excelle dans la gestion du DOM virtuel, nous devons parfois rendre des éléments en dehors de la hiérarchie normale des composants. C'est là que createPortal entre en jeu. Ce guide explore createPortal en profondeur, couvrant son objectif, son utilisation et les techniques avancées pour gérer les événements et construire des éléments d'interface utilisateur complexes. Nous aborderons les considérations d'internationalisation, les meilleures pratiques en matière d'accessibilité et les pièges courants à éviter.
Qu'est-ce que React createPortal ?
createPortal est une API React qui vous permet de rendre les enfants d'un composant React dans une autre partie de l'arborescence du DOM, en dehors de la hiérarchie du composant parent. Ceci est particulièrement utile pour créer des éléments comme des modales, des infobulles, des menus déroulants et des superpositions qui doivent être positionnés au niveau supérieur du document ou dans un conteneur spécifique, quel que soit l'endroit où se trouve le composant qui les déclenche dans l'arborescence des composants React.
Sans createPortal, atteindre cet objectif implique souvent des solutions de contournement complexes telles que la manipulation directe du DOM ou l'utilisation du positionnement absolu en CSS, ce qui peut entraîner des problèmes de contextes d'empilement, de conflits de z-index et d'accessibilité.
Pourquoi utiliser createPortal ?
Voici les principales raisons pour lesquelles createPortal est un outil précieux dans votre arsenal React :
- Structure du DOM améliorée : Évite d'imbriquer profondément les composants dans le DOM, ce qui conduit à une structure plus propre et plus facile à gérer. C'est particulièrement important pour les applications complexes avec de nombreux éléments interactifs.
- Stylisation simplifiée : Positionnez facilement les éléments par rapport à la fenêtre d'affichage (viewport) ou à des conteneurs spécifiques sans recourir à des astuces CSS complexes. Cela simplifie la stylisation et la mise en page, en particulier lorsqu'il s'agit d'éléments qui doivent se superposer à d'autres contenus.
- Accessibilité améliorée : Facilite la création d'interfaces utilisateur accessibles en vous permettant de gérer le focus et la navigation au clavier indépendamment de la hiérarchie des composants. Par exemple, en s'assurant que le focus reste à l'intérieur d'une fenêtre modale.
- Meilleure gestion des événements : Permet aux événements de se propager correctement depuis le contenu du portail vers l'arborescence React, garantissant que les écouteurs d'événements attachés aux composants parents fonctionnent toujours comme prévu.
Utilisation de base de createPortal
L'API createPortal accepte deux arguments :
- Le nœud React (JSX) que vous souhaitez rendre.
- L'élément DOM où vous souhaitez rendre le nœud. Cet élément DOM devrait idéalement exister avant que le composant utilisant
createPortalne soit monté.
Voici un exemple simple :
Exemple : Rendre une modale
Supposons que vous ayez un composant de modale que vous souhaitez rendre à la fin de l'élément body.
import React from 'react';
import ReactDOM from 'react-dom';
function Modal({ children, isOpen, onClose }) {
if (!isOpen) return null;
const modalRoot = document.getElementById('modal-root'); // Suppose que vous avez un <div id="modal-root"></div> dans votre HTML
if (!modalRoot) {
console.error('Élément racine de la modale non trouvé !');
return null;
}
return ReactDOM.createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>,
modalRoot
);
}
export default Modal;
Explication :
- Nous importons
ReactDOMcarcreatePortalest une méthode de l'objetReactDOM. - Nous supposons qu'il y a un élément DOM avec l'ID
modal-rootdans votre HTML. C'est là que la modale sera rendue. Assurez-vous que cet élément existe. Une pratique courante est d'ajouter un<div id="modal-root"></div>juste avant la balise de fermeture</body>dans votre fichierindex.html. - Nous utilisons
ReactDOM.createPortalpour rendre le JSX de la modale dans l'élémentmodalRoot. - Nous utilisons
e.stopPropagation()pour empêcher l'événementonClicksur le contenu de la modale de déclencher le gestionnaireonClosesur la superposition. Cela garantit qu'un clic à l'intérieur de la modale ne la ferme pas.
Utilisation :
import React, { useState } from 'react';
import Modal from './Modal';
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<button onClick={() => setIsModalOpen(true)}>Ouvrir la modale</button>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<h2>Contenu de la modale</h2>
<p>Ceci est le contenu de la modale.</p>
<button onClick={() => setIsModalOpen(false)}>Fermer</button>
</Modal>
</div>
);
}
export default App;
Cet exemple montre comment rendre une modale en dehors de la hiérarchie normale des composants, vous permettant de la positionner de manière absolue sur la page. Utiliser createPortal de cette manière résout les problèmes courants de contextes d'empilement et vous permet de créer facilement un style de modale cohérent dans toute votre application.
Gestion des événements avec createPortal
L'un des principaux avantages de createPortal est qu'il préserve le comportement normal de propagation des événements (event bubbling) de React. Cela signifie que les événements provenant du contenu du portail se propageront toujours vers le haut de l'arborescence des composants React, permettant aux composants parents de les gérer.
Cependant, il est important de comprendre comment les événements sont gérés lorsqu'ils traversent la frontière du portail.
Exemple : Gérer les événements en dehors du portail
import React, { useState, useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
function OutsideClickExample() {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
const portalRoot = document.getElementById('portal-root');
useEffect(() => {
function handleClickOutside(event) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [dropdownRef]);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Basculer le menu déroulant</button>
{isOpen && portalRoot && ReactDOM.createPortal(
<div ref={dropdownRef} style={{ position: 'absolute', top: '50px', left: '0', border: '1px solid black', padding: '10px', backgroundColor: 'white' }}>
Contenu du menu déroulant
</div>,
portalRoot
)}
</div>
);
}
export default OutsideClickExample;
Explication :
- Nous utilisons une
refpour accéder à l'élément du menu déroulant rendu à l'intérieur du portail. - Nous attachons un écouteur d'événement
mousedownaudocumentpour détecter les clics en dehors du menu déroulant. - À l'intérieur de l'écouteur d'événement, nous vérifions si le clic s'est produit en dehors du menu déroulant en utilisant
dropdownRef.current.contains(event.target). - Si le clic s'est produit en dehors du menu déroulant, nous le fermons en mettant
isOpenàfalse.
Cet exemple montre comment gérer les événements qui se produisent en dehors du contenu du portail, vous permettant de créer des éléments interactifs qui répondent aux actions de l'utilisateur dans le document environnant.
Cas d'usage avancés
createPortal ne se limite pas aux simples modales et infobulles. Il peut être utilisé dans divers scénarios avancés, notamment :
- Menus contextuels : Rendre dynamiquement des menus contextuels près du curseur de la souris lors d'un clic droit.
- Notifications : Afficher des notifications en haut de l'écran, indépendamment de la hiérarchie des composants.
- Popovers personnalisés : Créer des composants de popover personnalisés avec un positionnement et un style avancés.
- Intégration avec des bibliothèques tierces : Utiliser
createPortalpour intégrer des composants React avec des bibliothèques tierces qui nécessitent des structures DOM spécifiques.
Exemple : Créer un menu contextuel
import React, { useState, useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
function ContextMenuExample() {
const [contextMenu, setContextMenu] = useState(null);
const menuRef = useRef(null);
useEffect(() => {
function handleClickOutside(event) {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setContextMenu(null);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [menuRef]);
const handleContextMenu = (event) => {
event.preventDefault();
setContextMenu({
x: event.clientX,
y: event.clientY,
});
};
const portalRoot = document.getElementById('portal-root');
return (
<div onContextMenu={handleContextMenu} style={{ border: '1px solid black', padding: '20px' }}>
Faites un clic droit ici pour ouvrir le menu contextuel
{contextMenu && portalRoot && ReactDOM.createPortal(
<div
ref={menuRef}
style={{
position: 'absolute',
top: contextMenu.y,
left: contextMenu.x,
border: '1px solid black',
padding: '10px',
backgroundColor: 'white',
}}
>
<ul>
<li>Option 1</li>
<li>Option 2</li>
<li>Option 3</li>
</ul>
</div>,
portalRoot
)}
</div>
);
}
export default ContextMenuExample;
Explication :
- Nous utilisons l'événement
onContextMenupour détecter les clics droits sur l'élément cible. - Nous empêchons le menu contextuel par défaut d'apparaître en utilisant
event.preventDefault(). - Nous stockons les coordonnées de la souris dans la variable d'état
contextMenu. - Nous rendons le menu contextuel à l'intérieur d'un portail, positionné aux coordonnées de la souris.
- Nous incluons la même logique de détection de clic extérieur que dans l'exemple précédent pour fermer le menu contextuel lorsque l'utilisateur clique en dehors de celui-ci.
Considérations sur l'accessibilité
Lorsque vous utilisez createPortal, il est crucial de prendre en compte l'accessibilité pour garantir que votre application est utilisable par tous.
Gestion du focus
Lorsqu'un portail s'ouvre (par exemple, une modale), vous devez vous assurer que le focus est automatiquement déplacé vers le premier élément interactif à l'intérieur du portail. Cela aide les utilisateurs qui naviguent avec un clavier ou un lecteur d'écran à accéder facilement au contenu du portail.
Lorsque le portail se ferme, vous devez redonner le focus à l'élément qui a déclenché l'ouverture du portail. Cela maintient un flux de navigation cohérent.
Attributs ARIA
Utilisez les attributs ARIA pour fournir des informations sémantiques sur le contenu du portail. Par exemple, utilisez aria-modal="true" sur l'élément de la modale pour indiquer qu'il s'agit d'une boîte de dialogue modale. Utilisez aria-labelledby pour associer la modale à son titre, et aria-describedby pour l'associer à sa description.
Navigation au clavier
Assurez-vous que les utilisateurs peuvent naviguer dans le contenu du portail à l'aide du clavier. Utilisez l'attribut tabindex pour contrôler l'ordre du focus, et assurez-vous que tous les éléments interactifs sont accessibles avec le clavier.
Envisagez de piéger le focus à l'intérieur du portail afin que les utilisateurs ne puissent pas naviguer accidentellement en dehors de celui-ci. Cela peut être réalisé en écoutant la touche Tab et en déplaçant programmatiquement le focus vers le premier ou le dernier élément interactif à l'intérieur du portail.
Exemple : Modale accessible
import React, { useState, useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
function AccessibleModal({ children, isOpen, onClose, labelledBy, describedBy }) {
const modalRef = useRef(null);
const firstFocusableElementRef = useRef(null);
const [previouslyFocusedElement, setPreviouslyFocusedElement] = useState(null);
const modalRoot = document.getElementById('modal-root');
useEffect(() => {
if (isOpen) {
// Sauvegarder l'élément actuellement focus avant d'ouvrir la modale.
setPreviouslyFocusedElement(document.activeElement);
// Mettre le focus sur le premier élément focusable dans la modale.
if (firstFocusableElementRef.current) {
firstFocusableElementRef.current.focus();
}
// Piéger le focus à l'intérieur de la modale.
function handleKeyDown(event) {
if (event.key === 'Tab') {
const focusableElements = modalRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstFocusableElement = focusableElements[0];
const lastFocusableElement = focusableElements[focusableElements.length - 1];
if (event.shiftKey) {
// Maj + Tab
if (document.activeElement === firstFocusableElement) {
lastFocusableElement.focus();
event.preventDefault();
}
} else {
// Tab
if (document.activeElement === lastFocusableElement) {
firstFocusableElement.focus();
event.preventDefault();
}
}
}
}
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
// Restaurer le focus sur l'élément qui avait le focus avant d'ouvrir la modale.
if(previouslyFocusedElement && previouslyFocusedElement.focus) {
previouslyFocusedElement.focus();
}
};
}
}, [isOpen, previouslyFocusedElement]);
if (!isOpen) return null;
return ReactDOM.createPortal(
<div
className="modal-overlay"
onClick={onClose}
aria-modal="true"
aria-labelledby={labelledBy}
aria-describedby={describedBy}
ref={modalRef}
>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h2 id={labelledBy}>Titre de la modale</h2>
<p id={describedBy}>Ceci est le contenu de la modale.</p>
<button ref={firstFocusableElementRef} onClick={onClose}>
Fermer
</button>
{children}
</div>
</div>,
modalRoot
);
}
export default AccessibleModal;
Explication :
- Nous utilisons des attributs ARIA comme
aria-modal,aria-labelledbyetaria-describedbypour fournir des informations sémantiques sur la modale. - Nous utilisons le hook
useEffectpour gérer le focus lorsque la modale s'ouvre et se ferme. - Nous sauvegardons l'élément actuellement focus avant d'ouvrir la modale et nous lui redonnons le focus lorsque la modale se ferme.
- Nous piégeons le focus à l'intérieur de la modale en utilisant un écouteur d'événement
keydown.
Considérations sur l'internationalisation (i18n)
Lors du développement d'applications pour un public mondial, l'internationalisation (i18n) est une considération essentielle. Lorsque vous utilisez createPortal, il y a quelques points à garder à l'esprit :
- Direction du texte (RTL/LTR) : Assurez-vous que votre style s'adapte aux langues de gauche à droite (LTR) et de droite à gauche (RTL). Cela peut impliquer l'utilisation de propriétés logiques en CSS (par exemple,
margin-inline-startau lieu demargin-left) et la définition appropriée de l'attributdirsur l'élément HTML. - Localisation du contenu : Tout le texte à l'intérieur du portail doit être localisé dans la langue préférée de l'utilisateur. Utilisez une bibliothèque i18n (par exemple,
react-intl,i18next) pour gérer les traductions. - Formatage des nombres et des dates : Formatez les nombres et les dates selon les paramètres régionaux de l'utilisateur. L'API
Intlfournit des fonctionnalités pour cela. - Conventions culturelles : Soyez conscient des conventions culturelles liées aux éléments de l'interface utilisateur. Par exemple, le placement des boutons peut différer d'une culture à l'autre.
Exemple : i18n avec react-intl
import React from 'react';
import { FormattedMessage } from 'react-intl';
function MyComponent() {
return (
<div>
<FormattedMessage id="myComponent.greeting" defaultMessage="Bonjour, le monde !" />
</div>
);
}
export default MyComponent;
Le composant FormattedMessage de react-intl récupère le message traduit en fonction des paramètres régionaux de l'utilisateur. Configurez react-intl avec vos traductions pour différentes langues.
Pièges courants et solutions
Bien que createPortal soit un outil puissant, il est important d'être conscient de certains pièges courants et de savoir comment les éviter :
- Élément racine du portail manquant : Assurez-vous que l'élément DOM que vous utilisez comme racine du portail existe avant que le composant utilisant
createPortalne soit monté. Une bonne pratique est de le placer directement dans le fichierindex.html. - Conflits de Z-Index : Soyez attentif aux valeurs de z-index lors du positionnement d'éléments avec
createPortal. Utilisez CSS pour gérer les contextes d'empilement et vous assurer que le contenu de votre portail s'affiche correctement. - Problèmes de gestion d'événements : Comprenez comment les événements se propagent à travers le portail et gérez-les de manière appropriée. Utilisez
e.stopPropagation()pour empêcher les événements de déclencher des actions non désirées. - Fuites de mémoire : Nettoyez correctement les écouteurs d'événements et les références lorsque le composant utilisant
createPortalest démonté pour éviter les fuites de mémoire. Utilisez le hookuseEffectavec une fonction de nettoyage pour y parvenir. - Problèmes de défilement inattendus : Les portails peuvent parfois interférer avec le comportement de défilement attendu de la page. Assurez-vous que vos styles n'empêchent pas le défilement et que les éléments modaux ne provoquent pas de sauts de page ou de comportements de défilement inattendus lorsqu'ils s'ouvrent et se ferment.
Conclusion
React.createPortal est un outil précieux pour créer des interfaces utilisateur flexibles, accessibles et maintenables en React. En comprenant son objectif, son utilisation et les techniques avancées pour la gestion des événements et l'accessibilité, vous pouvez exploiter sa puissance pour construire des applications web complexes et engageantes qui offrent une expérience utilisateur supérieure pour un public mondial. N'oubliez pas de prendre en compte les meilleures pratiques en matière d'internationalisation et d'accessibilité pour vous assurer que vos applications sont inclusives et utilisables par tous.
En suivant les directives et les exemples de ce guide, vous pouvez utiliser createPortal en toute confiance pour résoudre les défis courants de l'interface utilisateur et créer des expériences web époustouflantes.