Spis treści
- Wprowadzenie
- React Portals
- Alternatywy dla React Portals
- Kod przykładu na Github
- Klikalny przykład na Github Pages
- Podsumowanie
Wprowadzenie
Portale w React są funkcjonalnością, na którą natkniesz się prawdopodobnie na samym końcu – po przekopaniu całego internetu w poszukiwaniu rozwiązania. Renderowanie zawartości w innym miejscu niż logicznie element należy wydaje się nie mieć sensu – pomaga jednak rozwiązać problemy z z-index czy overflow na jakie możesz natknąć się przy próbach wyświetlenia treści. Choć Portal niekoniecznie będzie pierwszym wyborem – często może okazać się znacznie szybszym rozwiązaniem niż refaktor lub inne bardziej oczywiste opcje – poznajmy więc jak one działają.
React Portals
Portale pozwalają wyświetlić element w podanym rodzicu, który może być zlokalizowany w dowolnym miejscu. Aby móc to wykorzystać stwórzmy w pliku index.html element ulokowany poza rootem głównej aplikacji Reacta. Dla przypomnienia, domyślnie aplikacja CRA zawiera index.html z div z id="root", w którym wyrenderuje się aplikacja. Dodajmy element poza głównym rootem oraz kolory, które pozwolą rozróżnić oba elementy:
<body>
<div id="root" style="border: 1px solid red; padding: 20px"></div>
<div style="border: 1px solid blue; padding: 20px">
<h2>
Outside React app
</h2>
<div id="outside-root">
Something inside, but won't be overriden
</div>
</div>
</body>
Przypomnijmy, że React użyje elementu z id="root":
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
W kolejnym kroku stworzymy OutsideToastsComponent zawierający „toasty”, komponent umieścimy wewnątrz div na którym przypniemy wyświetlanie alertu na kliknięcie – by pokazać, że propagacja eventów w górę działa poprawnie mimo że elementy nie będą w drzewie DOM tego elementu:
<div
onClick={() => alert('I was clicked! Even from outside my DOM which means event propagates properly')}>
<OutsideToastsComponent />
</div>
Sam komponent będzie zawierał standardowe elementy stąd nie będziemy całego tu wklejać – kod na Github i przykład klikalny na Github Pages będą dostępne – warto natomiast zwrócić uwagę na to, co jest zwracane:
return ReactDOM.createPortal(
<>
<br />
<button onClick={onAddClick}>
Add toast
</button>
{toastList}
</>,
document.querySelector('#outside-root'),
);
To, co w normalnym kodzie byłoby zwrócone bezpośrednio, jest przekazane jako pierwszy parametr do funkcji createPortal. Drugim parametrem jest miejsce gdzie elementy mają zostać wyrenderowane.
I faktycznie, elementy renderują się poza głównym drzewem aplikacji czyli div z id="root":

Natomiast jeśli chodzi o podgląd drzewa komponentów React, komponent nasz pokazuje się w drzewie aplikacji:

Alternatywy dla React Portals
Alternatywnym podejściem do powyższego jest pozostawienie toastów w aplikacji głównej w normalnym flow Reactowym, ale umieszczenie komponentu z nimi niezbyt głęboko w drzewie komponentów.
Z pomocą w implementacji takiego podejścia pomoże nam globalne zarządzanie stanem. Rozważymy tutaj dwie opcje:
- React Context
- React Redux
React Context
React Context pokrótce przedstawiłem w jednym ze swoich postów:
Tutaj wystawimy przykładowy Context:
export const ToastsContext = createContext({
// default values
toasts: [],
addToast: () => {},
removeToast: el => {},
toastsVisible: false,
});
Nadamy kontekstowi wartości, które przechowywane będą w stanie głównego komponentu:
const [contextToastsVisible, setContextToastsVisible] = useState(false); const [contextToasts, setContextToasts] = useState([]);
<ToastsContext.Provider value={{
toasts: contextToasts,
addToast: () => setContextToasts(toasts => [...toasts, new Date().getTime()]),
toastsVisible: contextToastsVisible,
removeToast: (id) => setContextToasts(toasts => toasts.filter(toast => toast !== id))
}}>
<ContextToastsComponent />
</ToastsContext.Provider>
Sam komponent zaś składa się z 2 mniejszych komponentów, które wartości odczytają bezpośrednio z kontekstu:
export const ContextToastsComponent = () => (
<>
<br />
<AddToast />
<ToastList />
</>
)
const useToastsContext = () => useContext(ToastsContext);
const AddToast = () => {
const {addToast} = useToastsContext();
return <button onClick={addToast}>Add toast</button>;
}
const ToastList = () => {
const {toasts, toastsVisible, removeToast} = useToastsContext();
if (!toastsVisible) {
return null;
}
return toasts.map(toast => (
<div key={toast}>
Toast with id: {toast}
<br />
<button onClick={() => removeToast(toast)}>Remove me</button>
</div>
))
}
React Redux
Wygenerowany przez CRA projekt komendą
npx create-react-app my-app --template redux
Będzie domyślnie używał biblioteki Redux Toolkit. Biblioteka ta pochodzi od twórców Reacta i znacząco usprawnia pracę z nim. Jeśli chcesz poznać Redux Toolkit lepiej, kliknij w poniższy obrazek:

Samo pisane kodu rozpocznijmy od dodania slice, który będzie obsługiwał stan:
const initialState = {
toastsVisible: false,
list: [],
};
export const toastsSlice = createSlice({
name: 'toasts',
initialState,
reducers: {
toggleToasts: state => {
state.toastsVisible = !state.toastsVisible;
},
addToast: state => {
state.list.push(new Date().getTime());
},
removeToast: (state, {payload}) => {
state.list = state.list.filter(toast => toast !== payload);
},
},
});
export const { toggleToasts, addToast, removeToast } = toastsSlice.actions;
export const toastsReducer = toastsSlice.reducer;
Po czym dane możemy odczytywać w komponencie:
const toasts = useSelector(state => state.toasts.list);
Sam kod komponentu, który jest prosty – zawiera dispatchowanie innych akcji i wyświetlenie listy – dostępny będzie na Github.
Zanim się rozstaniemy – na potrzeby tego przykładu użyłem hooka useSelector – w pracy jednak nie przechodzimy na razie z connect na useSelector mimo, ze wersje bibliotek pozwalają. Dlaczego? Ponieważ niesie to ze sobą kilka zagrożeń, które opisałem w artykule:
Kod przykładu na Github
https://github.com/radek-anuszewski/react-portals
Klikalny przykład na Github Pages
https://radek-anuszewski.github.io/react-portals/
Podsumowanie
Choć Portale nie wydają się być rozwiązaniem pierwszego wyboru, warto zdawać sobie sprawę z ich istnienia. Choć do problemów które rozwiązują można podejść inaczej – na przykład za pomocą globalnego stanu – mogą pomóc w sytuacji, gdy inne rozwiązania będą kosztowne.
