Portals & Refs
Portals
- React Portals are a powerful feature in React that allows you to render components outside the current React tree hierarchy.
- With portals, you can easily render elements into different parts of the DOM, such as modals, tooltips, or any other component that needs to break out of the component's container.
First thing, let's go into index.html
and add a separate mount point:
<!-- above #root -->
<div id="modal"></div>
This where the modal will actually be mounted whenever we render to this portal. Totally separate from our app root.
src/components/Modal.jsx
import React, { useEffect, useRef } from "react";
import { createPortal } from "react-dom";
const Modal = ({ children }) => {
const elRef = useRef(null);
if (!elRef.current) {
elRef.current = document.createElement("div");
}
useEffect(() => {
const modalRoot = document.getElementById("modal");
modalRoot.appendChild(elRef.current);
return () => modalRoot.removeChild(elRef.current);
}, []);
return createPortal(<div>{children}</div>, elRef.current);
};
export default Modal;
- This will mount a div and mount inside of the portal whenever the Modal is rendered and then remove itself whenever it's unrendered.
- We're using the feature of useEffect that if you need to clean up after you're done (we need to remove the div once the Modal is no longer being rendered) you can return a function inside of useEffect that cleans up.
- We're also using a ref here via the hook useRef. Refs are like instance variables for function components. Whereas on a class you'd say this.myVar to refer to an instance variable, with function components you can use refs. They're containers of state that live outside a function's closure state which means anytime I refer to elRef.current, it's always referring to the same element. This is different from a useState call because the variable returned from that useState call will always refer to the state of the variable when that function was called. It seems like a weird hair to split but it's important when you have async calls and effects because that variable can change and nearly always you want the useState variable, but with something like a portal it's important we always refer to the same DOM div; we don't want a lot of portals.
- Down at the bottom we use React's createPortal to pass the children (whatever you put inside
<Modal></Modal>
) to the portal div.
Details.jsx
import { useState } from "react";
import Modal from "./Modal";
// add showModal
const [showModal, setShowModal] = useState(false);
// add onClick to <button>
<button onClick={() => setShowModal(true)}>Adopt {pet.name}</button>;
// below description
{
showModal ? (
<Modal>
<div>
<h1>Would you like to adopt {pet.name}?</h1>
<div className="buttons">
<button>Yes</button>
<button onClick={() => setShowModal(false)}>No</button>
</div>
</div>
</Modal>
) : null; // you have to remove this semi-colon, my auto-formatter adds it back if I delete it
}