Apprends à rendre des composants React en dehors de leur hiérarchie DOM parente en utilisant les portails — essentiel pour les modales, les infobulles et les menus déroulants qui doivent s'échapper des contraintes d'overflow ou de z-index.
Normalement, quand tu rends un composant enfant dans React, il est monté dans le DOM en tant qu'enfant du nœud DOM parent le plus proche :
function Parent() {
return (
<div className="parent">
<Child />
</div>
);
}Cela signifie que <Child /> sera toujours rendu à l'intérieur de la div .parent dans le DOM réel. La plupart du temps, c'est exactement ce que tu veux. Mais parfois, tu as besoin qu'un composant se rende en dehors de la hiérarchie DOM de son parent.
Les portails te permettent de rendre un enfant dans un nœud DOM différent, n'importe où dans le document, tout en le gardant comme partie de l'arbre de composants React. Le composant reste connecté à son parent pour le contexte, les événements et l'état — mais sa sortie DOM réelle vit ailleurs.
ReactDOM.createPortal(child, domNode)child — N'importe quel élément React rendable (JSX, chaîne de caractères, fragment, etc.)domNode — L'élément DOM où l'enfant doit être montéC'est l'une de ces API qui semble obscure jusqu'à ce que tu rencontres le problème exact qu'elle résout — puis elle devient indispensable.
La raison la plus courante d'utiliser les portails est le problème d'overflow/z-index avec les modales, les infobulles et les menus déroulants.
Considère cette structure :
<div class="card" style="overflow: hidden; position: relative;">
<button>Ouvrir le Menu</button>
<div class="dropdown-menu">
<!-- Ceci est coupé par overflow: hidden! -->
</div>
</div>Si le parent a overflow: hidden, ton menu déroulant est coupé. S'il a un z-index bas, ta modale peut apparaître derrière d'autres éléments. Ce sont des contraintes de mise en page CSS que tu ne peux pas corriger simplement en ajoutant plus de CSS — tu as besoin que l'élément vive en dehors du parent contraignant dans le DOM.
Sans les portails, tu devrais :
Les portails résolvent cela élégamment — ton composant modale reste logiquement là où il appartient dans l'arbre React (à l'intérieur du composant carte), mais sa sortie DOM se rend au niveau supérieur du document.
Cas d'usage courants pour les portails :
Pour utiliser un portail, tu as besoin de deux choses : un nœud DOM cible et un appel à ReactDOM.createPortal().
Étape 1 : Créer un nœud DOM cible. Dans ton HTML, ajoute un conteneur séparé à côté de ta racine React :
<div id="root"></div>
<div id="modal-root"></div>Étape 2 : Créer un composant portail.
function Modal({ children, isOpen }) {
if (!isOpen) return null;
return ReactDOM.createPortal(
<div className="modal-overlay">
<div className="modal-content">
{children}
</div>
</div>,
document.getElementById('modal-root')
);
}Étape 3 : Utilise-le comme n'importe quel autre composant.
function App() {
const [showModal, setShowModal] = React.useState(false);
return (
<div>
<h1>Mon App</h1>
<button onClick={() => setShowModal(true)}>Ouvrir la Modale</button>
<Modal isOpen={showModal}>
<h2>Titre de la Modale</h2>
<p>Ceci est rendu dans #modal-root, pas dans #root !</p>
<button onClick={() => setShowModal(false)}>Fermer</button>
</Modal>
</div>
);
}Même si <Modal> est utilisé à l'intérieur de <App>, l'élément <div className="modal-overlay"> réel apparaîtra à l'intérieur de #modal-root dans le DOM, complètement en dehors de l'élément #root.
Tu peux vérifier cela en inspectant le DOM — le contenu de la modale sera un enfant de #modal-root, pas de #root.
L'une des fonctionnalités les plus puissantes (et parfois surprenantes) des portails est que les événements se propagent à travers l'arbre de composants React, pas l'arbre DOM.
Cela signifie que même si la sortie DOM d'un portail vit dans une partie différente du document, les événements déclenchés à l'intérieur du portail se propageront toujours à travers la hiérarchie de composants React comme si le portail était un enfant régulier.
function Parent() {
const handleClick = (e) => {
// Ceci SERA déclenché lors d'un clic à l'intérieur du portail !
console.log('Parent a capturé le clic:', e.target);
};
return (
<div onClick={handleClick}>
<h1>Composant Parent</h1>
<Modal isOpen={true}>
<button>Clique-moi (je suis dans un portail)</button>
</Modal>
</div>
);
}Quand l'utilisateur clique sur le bouton à l'intérieur du portail, l'événement de clic se propage à travers l'arbre de composants React jusqu'au <div onClick={handleClick}> dans Parent — même si le bouton n'est pas un descendant DOM de cette div.
Ce comportement est intentionnel et très utile :
Cependant, cela peut aussi causer des comportements inattendus. Si un composant parent a un gestionnaire de clic qui arrête la propagation, il affectera aussi les événements du portail. Garde cela à l'esprit lors du débogage de problèmes liés aux événements avec les portails.
Une modale prête pour la production nécessite plus qu'un simple portail. Les exigences d'accessibilité incluent :
1. Piégeage du focus : Quand la modale s'ouvre, le focus doit se déplacer dedans et y rester jusqu'à sa fermeture. Tab et Shift+Tab doivent faire défiler les éléments focusables à l'intérieur de la modale.
2. Touche Escape : Appuyer sur Escape devrait fermer la modale.
3. Attributs ARIA : La modale a besoin de role="dialog", aria-modal="true", et aria-labelledby pointant vers son en-tête.
4. Verrouillage du défilement en arrière-plan : La page derrière la modale ne devrait pas défiler.
function AccessibleModal({ isOpen, onClose, title, children }) {
const modalRef = React.useRef(null);
React.useEffect(() => {
if (!isOpen) return;
const handleEscape = (e) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', handleEscape);
document.body.style.overflow = 'hidden';
modalRef.current?.focus();
return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = '';
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return ReactDOM.createPortal(
<div className="modal-overlay" onClick={onClose}>
<div
ref={modalRef}
className="modal"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
tabIndex={-1}
onClick={(e) => e.stopPropagation()}
>
<h2 id="modal-title">{title}</h2>
{children}
<button onClick={onClose}>Fermer</button>
</div>
</div>,
document.getElementById('modal-root')
);
}Le onClick sur la superposition ferme la modale lors d'un clic à l'extérieur, tandis que e.stopPropagation() sur le contenu de la modale empêche la fermeture lors d'un clic à l'intérieur. C'est un pattern standard utilisé par la plupart des bibliothèques de modales.
<div id="root"></div>
<div id="modal-root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
function Modal({ children }) {
return ReactDOM.createPortal(
<div id="modal" className="modal"
style={{ padding: "20px", background: "#222", border: "1px solid #555", borderRadius: "8px" }}>
{children}
</div>,
document.getElementById('modal-root')
);
}
function App() {
return (
<div>
<h1>Contenu de l'App (à l'intérieur de #root)</h1>
<p>La modale ci-dessous est rendue dans #modal-root via un portail :</p>
<Modal>
<p>Contenu de la Modale</p>
<p>Je vis dans #modal-root, pas #root !</p>
</Modal>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>Lors de l'utilisation d'un portail, où les événements React se propagent-ils ?