Wraz z nadejściem Reacta w wersji 16 pojawiła się funkcjonalność o dość osobliwej nazwie - Portal. Pierwsze, co przyszło mi na myśl, gdy o niej usłyszałem, to film Gwiezdne Wrota i odkopany przez (jakżeby inaczej) amerykańskich naukowców kamienny portal do podróży międzygwiezdnych. Bądź co bądź, nazwa dość dobrze oddaje cel, w jakim funkcjonalność została stworzona. Portal w ReactJS umożliwia nam przeniesienie elementów strony w miejsce bardzo odległe od komponentu - rodzica, będące poza jego hierarchią, a jednocześnie zachowuje się jak jego komponent - dziecko.
Czasem mamy potrzebę, aby pewien element strony wyświetlić ponad wszystkimi, jak w przypadku modala. W React możemy mieć do czynienia ze skomplikowaną strukturą komponentów, np.:
<App>
<HomePage>
<Form>
<FormGroup>
<FormInput />
<InputAlert />
</FormGroup>
</Form>
</HomePage>
</App>
Przy tej zawiłej hierarchii i stylowaniu komponentów - rodziców (które mogą posiadać chociażby atrybut overflow: hidden
) wywołanie ostrzegającego nas przed jakąś akcją modala w komponencie <InputAlert />
może okazać się problemem nie do obejścia. Z pomocą przyjdzie nam właśnie funkcjonalność createPortal, która wyświetli element poza strukturę DOM aplikacji.
Zakładając, że główny komponent renderowany jest w kontenerze o id='root
', dodajmy w linii poniżej kontener, w którym będziemy chcieli wyświetlić nasz modal:
<body>
<div id="root"></div>
<div id="portal"></div>
</body>
Teraz stworzymy komponent o nazwie Portal, w którym zaimplementujemy jego działanie. Będzie pośrednikiem pomiędzy komponentem reactowym, a elementem w drzewie DOM, gdzie wyświetli się modal.
Potrzebujemy zaimportować pakiet ReactDOM
, gdzie znajduje się funkcjonalność tworzenia portali.
import ReactDOM from 'react-dom';
Komponent musi wiedzieć, gdzie ma otworzyć portal - podajemy więc element w DOM, gdzie się otworzy:
const portalRoot = document.getElementById('portal');
Jako, że nie będziemy korzystać ze stanu komponentu, tworzymy functional component:
export const Portal = props => {
const {
children
} = props; //
skorzystamy z destrukturyzacji ES6 return ReactDOM.createPortal(children, portalRoot); //
tworzymy portal i wrzucamy element, który zostanie wyświetlony przez portal };
Przekazujemy w propie children element, jaki ma być wyświetlony przez portal i - korzystając z funkcji createPortal - przekazujemy tenże element w pierwszym parametrze. W drugim podamy element w drzewie DOM, w którym otworzy się portal.
Kolejnym krokiem będzie dodanie komponentu Modal - elementu wyświetlanego przez portal, zdefiniowanego akapit wyżej jako prop children. Będzie on komponentem bezstanowym - także tu również skorzystamy z arrow function:
import React from 'react';
import PropTypes from 'prop-types';
export const Modal = props => {
const {
toggleModal,
visibility
} = props;
return (
visibility && < div className = "modal-component" >
<
div className = "modal-component__body" >
<
h1 > Modal title < / h1 > < div > It works! < / div > < button className= "btn" > Click and do nuffin' </
button > < button className = "btn"
onClick = {
toggleModal
} > Close < / button > <
/ div > </
div > );
};
PropTypes.propTypes = {
visibility: PropTypes.bool,
toggleModal: PropTypes.func.isRequired
};
Komponent przyjmuje dwa propy visibility
oraz toggleModal
, obsługujące wyświetlanie. Wykorzystany został tutaj też pakiet prop-types do kontrolowania typów ww. propów.
Następnie komponent Modal
umieszczamy jako children w komponencie <Portal>
. W ten sposób komponent Modal
zostanie przekazany do komponentu Portal
.
<Portal>
< Modal toggleModal={this.onModalClick} visibility={this.state.showModal}>
< h1> Modal title </ h1>
< div> It works ! </ div>
</ Modal>
< /Portal>
Dodamy jeszcze przycisk, który będzie sterował wyświetlaniem Modala:
<button className="btn" onClick={ this .onModalClick}>
Wyświetl modal < /button>
Po kliknięciu możemy zobaczyć, że modal faktycznie wyświetla się poza divem 'root'. Ponadto zachowuje dostęp do zmiennych i funkcji zdefiniowanych w ramach komponentu rodzica.
Co ciekawe, portal - który przecież znajduje się teraz poza głównym kontenerem 'root' - nadal zachowuje się jak jego element. Weźmy chociażby zjawisko event bubblingu. Dodajmy w komponencie Modal przycisk, który nie będzie miał zdefiniowanej żadnej akcji:
<button className= "btn" >Click me and I 'll do nuffin' < /button>
Następnie w komponencie rodzicu złapiemy event, który zgodnie z teorią event bubblingu przeszedł do komponentu - rodzica ze względu na brak zdefiniowanej akcji onClick:
state = {
clicks: 0,
showModal: false
};
onModalClick = () => {
this.setState(prevState => ({
showModal: !prevState.showModal
}));
};
handleClicking = () => {
this.setState(prevState => ({
clicks: prevState.clicks + 1
}));
};
Wyświetlając liczbę kliknięć ustawianą w stanie:
<strong> Number of clicks: { this .state.clicks}< /strong>
zauważymy, że - pomimo zupełnie innego położenia - Portal nadal zachowuje się tak, jakby był elementem React Tree.
Wersja 16 ReactJS przyniosła nam kilka ciekawych udogodnień, między innymi właśnie funkcjonalność Portal. Nie dość, że znacznie ułatwia ona pracę z elementami typu modal, popover czy tooltip, to zachowuje zalety elementu drzewa reactowego. Dużym plusem jest również jej łatwość implementacji i czytelność. W zasadzie jedną linią kodu otwieramy Portal i wrzucamy do niego zawartość. Elementy takie Portal sprawiają, że programowanie z wykorzystaniem biblioteki React jest łatwe i przyjemne.