En omfattende guide til Reacts createPortal API, der dækker teknikker til oprettelse af portaler, strategier for hændelseshåndtering og avancerede anvendelsestilfælde til at bygge fleksible og tilgængelige brugergrænseflader.
React createPortal: Mestring af portaloprettelse og hændelseshåndtering
I moderne webudvikling med React er det afgørende at skabe brugergrænseflader, der integreres problemfrit med den underliggende dokumentstruktur. Mens Reacts komponentmodel er fremragende til at administrere den virtuelle DOM, har vi nogle gange brug for at gengive elementer uden for det normale komponenthierarki. Det er her, createPortal kommer ind i billedet. Denne guide udforsker createPortal i dybden og dækker dets formål, brug og avancerede teknikker til håndtering af hændelser og opbygning af komplekse UI-elementer. Vi vil dække overvejelser om internationalisering, bedste praksis for tilgængelighed og almindelige faldgruber, der skal undgås.
Hvad er React createPortal?
createPortal er et React API, der giver dig mulighed for at gengive en React-komponents børn i en anden del af DOM-træet, uden for forældrekomponentens hierarki. Dette er især nyttigt til at skabe elementer som modalvinduer, tooltips, dropdown-menuer og overlays, der skal placeres på øverste niveau i dokumentet eller i en specifik container, uanset hvor den komponent, der udløser dem, er placeret i React-komponenttræet.
Uden createPortal indebærer det ofte komplekse løsninger som at manipulere DOM'en direkte eller bruge CSS absolute positioning, hvilket kan føre til problemer med stacking contexts, z-index-konflikter og tilgængelighed.
Hvorfor bruge createPortal?
Her er de vigtigste grunde til, at createPortal er et værdifuldt værktøj i dit React-arsenal:
- Forbedret DOM-struktur: Undgår at indlejre komponenter dybt i DOM'en, hvilket fører til en renere og mere håndterbar struktur. Dette er især vigtigt for komplekse applikationer med mange interaktive elementer.
- Forenklet styling: Positionér nemt elementer i forhold til viewporten eller specifikke containere uden at være afhængig af komplekse CSS-tricks. Dette forenkler styling og layout, især når man arbejder med elementer, der skal lægges over andet indhold.
- Forbedret tilgængelighed: Gør det lettere at skabe tilgængelige brugergrænseflader ved at give dig mulighed for at administrere fokus og tastaturnavigation uafhængigt af komponenthierarkiet. For eksempel at sikre, at fokus forbliver inden i et modalvindue.
- Bedre hændelseshåndtering: Giver hændelser mulighed for at propagere korrekt fra portalens indhold til React-træet, hvilket sikrer, at event listeners, der er knyttet til forældrekomponenter, stadig fungerer som forventet.
Grundlæggende brug af createPortal
createPortal API'et accepterer to argumenter:
- Den React-node (JSX), du vil gengive.
- Det DOM-element, hvor du vil gengive noden. Dette DOM-element bør ideelt set eksistere, før komponenten, der bruger
createPortal, mounter.
Her er et simpelt eksempel:
Eksempel: Gengivelse af et modalvindue
Lad os sige, du har en modal-komponent, som du vil gengive i slutningen af body-elementet.
import React from 'react';
import ReactDOM from 'react-dom';
function Modal({ children, isOpen, onClose }) {
if (!isOpen) return null;
const modalRoot = document.getElementById('modal-root'); // Antager, at du har en <div id="modal-root"></div> i din HTML
if (!modalRoot) {
console.error('Modal root element not found!');
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;
Forklaring:
- Vi importerer
ReactDOM, fordicreatePortaler en metode påReactDOM-objektet. - Vi antager, at der er et DOM-element med ID'et
modal-rooti din HTML. Det er her, modalvinduet vil blive gengivet. Sørg for, at dette element eksisterer. En almindelig praksis er at tilføje<div id="modal-root"></div>lige før det afsluttende</body>-tag i dinindex.html-fil. - Vi bruger
ReactDOM.createPortaltil at gengive modalvinduets JSX imodalRoot-elementet. - Vi bruger
e.stopPropagation()for at forhindre, atonClick-hændelsen på modalindholdet udløseronClose-handleren på overlayet. Dette sikrer, at et klik inde i modalvinduet ikke lukker det.
Brug:
import React, { useState } from 'react';
import Modal from './Modal';
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<button onClick={() => setIsModalOpen(true)}>Open Modal</button>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<h2>Modal Content</h2>
<p>This is the content of the modal.</p>
<button onClick={() => setIsModalOpen(false)}>Close</button>
</Modal>
</div>
);
}
export default App;
Dette eksempel viser, hvordan man gengiver et modalvindue uden for det normale komponenthierarki, hvilket giver dig mulighed for at positionere det absolut på siden. At bruge createPortal på denne måde løser almindelige problemer med stacking contexts og giver dig mulighed for nemt at skabe ensartet modal-styling på tværs af din applikation.
Hændelseshåndtering med createPortal
En af de vigtigste fordele ved createPortal er, at det bevarer den normale event bubbling-adfærd i React. Det betyder, at hændelser, der stammer fra portalens indhold, stadig vil propagere op gennem React-komponenttræet, hvilket giver forældrekomponenter mulighed for at håndtere dem.
Det er dog vigtigt at forstå, hvordan hændelser håndteres, når de krydser portalgrænsen.
Eksempel: Håndtering af hændelser uden for portalen
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)}>Toggle Dropdown</button>
{isOpen && portalRoot && ReactDOM.createPortal(
<div ref={dropdownRef} style={{ position: 'absolute', top: '50px', left: '0', border: '1px solid black', padding: '10px', backgroundColor: 'white' }}>
Dropdown Content
</div>,
portalRoot
)}
</div>
);
}
export default OutsideClickExample;
Forklaring:
- Vi bruger en
reftil at få adgang til dropdown-elementet, der er gengivet inde i portalen. - Vi tilføjer en
mousedownevent listener tildocumentfor at registrere klik uden for dropdown-menuen. - Inde i event listeneren tjekker vi, om klikket skete uden for dropdown-menuen ved hjælp af
dropdownRef.current.contains(event.target). - Hvis klikket skete uden for dropdown-menuen, lukker vi den ved at sætte
isOpentilfalse.
Dette eksempel viser, hvordan man håndterer hændelser, der sker uden for portalens indhold, hvilket giver dig mulighed for at skabe interaktive elementer, der reagerer på brugerhandlinger i det omgivende dokument.
Avancerede anvendelsestilfælde
createPortal er ikke begrænset til simple modalvinduer og tooltips. Det kan bruges i forskellige avancerede scenarier, herunder:
- Kontekstmenuer: Gengiv dynamisk kontekstmenuer nær musemarkøren ved højreklik.
- Notifikationer: Vis notifikationer øverst på skærmen, uanset komponenthierarkiet.
- Brugerdefinerede popovers: Opret brugerdefinerede popover-komponenter med avanceret positionering og styling.
- Integration med tredjepartsbiblioteker: Brug
createPortaltil at integrere React-komponenter med tredjepartsbiblioteker, der kræver specifikke DOM-strukturer.
Eksempel: Oprettelse af en kontekstmenu
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' }}>
Right-click here to open context menu
{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;
Forklaring:
- Vi bruger
onContextMenu-hændelsen til at registrere højreklik på målelementet. - Vi forhindrer standardkontekstmenuen i at blive vist ved hjælp af
event.preventDefault(). - Vi gemmer musekoordinaterne i
contextMenu-state-variablen. - Vi gengiver kontekstmenuen inde i en portal, positioneret ved musekoordinaterne.
- Vi inkluderer den samme logik til registrering af klik udenfor som i det foregående eksempel for at lukke kontekstmenuen, når brugeren klikker uden for den.
Overvejelser om tilgængelighed
Når du bruger createPortal, er det afgørende at overveje tilgængelighed for at sikre, at din applikation kan bruges af alle.
Fokushåndtering
Når en portal åbner (f.eks. et modalvindue), skal du sikre, at fokus automatisk flyttes til det første interaktive element i portalen. Dette hjælper brugere, der navigerer med tastatur eller skærmlæser, med nemt at få adgang til portalens indhold.
Når portalen lukker, skal du returnere fokus til det element, der udløste portalens åbning. Dette opretholder et konsekvent navigationsflow.
ARIA-attributter
Brug ARIA-attributter til at give semantisk information om portalens indhold. Brug for eksempel aria-modal="true" på modal-elementet for at angive, at det er en modal dialogboks. Brug aria-labelledby til at associere modalvinduet med dets titel og aria-describedby til at associere det med dets beskrivelse.
Tastaturnavigation
Sørg for, at brugere kan navigere i portalens indhold ved hjælp af tastaturet. Brug tabindex-attributten til at styre fokusrækkefølgen, og sørg for, at alle interaktive elementer kan nås med tastaturet.
Overvej at fange fokus inden i portalen, så brugere ikke ved et uheld kan navigere uden for den. Dette kan opnås ved at lytte efter Tab-tasten og programmatisk flytte fokus til det første eller sidste interaktive element i portalen.
Eksempel: Tilgængeligt modalvindue
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) {
// Save the currently focused element before opening the modal.
setPreviouslyFocusedElement(document.activeElement);
// Focus the first focusable element in the modal.
if (firstFocusableElementRef.current) {
firstFocusableElementRef.current.focus();
}
// Trap focus within the modal.
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) {
// Shift + 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);
// Restore focus to the element that had focus before opening the modal.
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}>Modal Title</h2>
<p id={describedBy}>This is the modal content.</p>
<button ref={firstFocusableElementRef} onClick={onClose}>
Close
</button>
{children}
</div>
</div>,
modalRoot
);
}
export default AccessibleModal;
Forklaring:
- Vi bruger ARIA-attributter som
aria-modal,aria-labelledbyogaria-describedbytil at give semantisk information om modalvinduet. - Vi bruger
useEffect-hooket til at administrere fokus, når modalvinduet åbner og lukker. - Vi gemmer det aktuelt fokuserede element, før modalvinduet åbner, og genopretter fokus på det, når modalvinduet lukker.
- Vi fanger fokus inden i modalvinduet ved hjælp af en
keydownevent listener.
Overvejelser om internationalisering (i18n)
Når man udvikler applikationer til et globalt publikum, er internationalisering (i18n) en kritisk overvejelse. Når man bruger createPortal, er der et par punkter, man skal huske på:
- Tekstretning (RTL/LTR): Sørg for, at din styling understøtter både venstre-til-højre (LTR) og højre-til-venstre (RTL) sprog. Dette kan indebære brug af logiske egenskaber i CSS (f.eks.
margin-inline-starti stedet formargin-left) og korrekt indstilling afdir-attributten på HTML-elementet. - Lokalisering af indhold: Al tekst i portalen skal lokaliseres til brugerens foretrukne sprog. Brug et i18n-bibliotek (f.eks.
react-intl,i18next) til at administrere oversættelser. - Formatering af tal og datoer: Formatér tal og datoer i overensstemmelse med brugerens lokalitet.
Intl-API'et giver funktionaliteter til dette. - Kulturelle konventioner: Vær opmærksom på kulturelle konventioner i forbindelse med UI-elementer. For eksempel kan placeringen af knapper variere på tværs af kulturer.
Eksempel: i18n med react-intl
import React from 'react';
import { FormattedMessage } from 'react-intl';
function MyComponent() {
return (
<div>
<FormattedMessage id="myComponent.greeting" defaultMessage="Hello, world!" />
</div>
);
}
export default MyComponent;
FormattedMessage-komponenten fra react-intl henter den oversatte besked baseret på brugerens lokalitet. Konfigurér react-intl med dine oversættelser for forskellige sprog.
Almindelige faldgruber og løsninger
Selvom createPortal er et kraftfuldt værktøj, er det vigtigt at være opmærksom på nogle almindelige faldgruber og hvordan man undgår dem:
- Manglende portal-rodelement: Sørg for, at det DOM-element, du bruger som portalens rod, eksisterer, før komponenten, der bruger
createPortal, mounter. En god praksis er at placere det direkte iindex.html. - Z-index-konflikter: Vær opmærksom på z-index-værdier, når du positionerer elementer med
createPortal. Brug CSS til at administrere stacking contexts og sikre, at din portals indhold vises korrekt. - Problemer med hændelseshåndtering: Forstå, hvordan hændelser propagerer gennem portalen, og håndtér dem korrekt. Brug
e.stopPropagation()for at forhindre hændelser i at udløse utilsigtede handlinger. - Hukommelseslækager: Ryd op i event listeners og referencer korrekt, når komponenten, der bruger
createPortal, unmounts, for at undgå hukommelseslækager. BruguseEffect-hooket med en oprydningsfunktion for at opnå dette. - Uventede scroll-problemer: Portaler kan nogle gange forstyrre den forventede scroll-adfærd på siden. Sørg for, at dine styles ikke forhindrer scrolling, og at modal-elementer ikke forårsager sidehop eller uventet scroll-adfærd, når de åbner og lukker.
Konklusion
React.createPortal er et værdifuldt værktøj til at skabe fleksible, tilgængelige og vedligeholdelsesvenlige brugergrænseflader i React. Ved at forstå dets formål, brug og avancerede teknikker til håndtering af hændelser og tilgængelighed kan du udnytte dets kraft til at bygge komplekse og engagerende webapplikationer, der giver en overlegen brugeroplevelse for et globalt publikum. Husk at overveje internationalisering og bedste praksis for tilgængelighed for at sikre, at dine applikationer er inkluderende og brugbare for alle.
Ved at følge retningslinjerne og eksemplerne i denne guide kan du trygt bruge createPortal til at løse almindelige UI-udfordringer og skabe fantastiske weboplevelser.