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.