One root, no div soup: fragments and portals

February 11, 2022 · 4 min read

Two small React features that looked unrelated until I put them side by side. Both are about the HTML that ends up in the browser, not the components I write. One removes elements I never wanted. The other moves an element to where it belongs.

Why JSX wants one root

I kept hitting the error for returning two sibling elements, and I treated it as a JSX quirk. It isn't. JSX compiles into React.createElement calls, and a function can only return one value. Two calls side by side is the same as writing return a b in plain JavaScript.

The obvious fix is to wrap everything in a div. It works, but in a bigger app every component does it, and the final page ends up with divs inside divs inside divs that carry no meaning. They can break styling, and the browser still has to render them all.

A wrapper that renders nothing

The fix I liked is a tiny component that returns only what was put inside it:

const Wrapper = (props) => {
  return props.children;
};

Now I return <Wrapper> as my single root and put the siblings inside. JSX is happy because there is one root. The DOM is happy because Wrapper leaves no element behind. In the elements tab you can see the difference right away.

React already ships this as Fragment, so I don't need my own. <React.Fragment> always works, and <> is the short form if the build setup supports it.

A modal buried in the page

Portals solve a different problem. Say a component returns a Modal next to an InputForm. In the real DOM, the modal ends up inside whatever section those components were rendered into, right between a heading and a form.

With the right CSS it looks like a modal, so for a while I thought that was good enough. But a modal sits over the whole page, and here it is buried in the structure. A screen reader sees an ordinary block of content. It is the same as styling a div like a button and attaching a click handler. It works, and it is still wrong.

WITHOUT A PORTAL#rootsectionmodalburied, only styledformWITH A PORTAL#overlay-rootmodaltop level#rootsectionformsiblings next to each other
The JSX is identical. A portal only changes where the modal's HTML ends up.

Teleporting it with createPortal

A portal lets me keep writing the components the same way, passing props normally, while the HTML lands somewhere else. It needs two things: a place to land, and a call that sends the element there. The place is just extra divs in index.html, next to the root:

<div id="backdrop-root"></div>
<div id="overlay-root"></div>
<div id="root"></div>

The call is createPortal from react-dom. The first argument is the JSX to render, and the second is the real DOM node to put it in. My modal ended up like this, with a Backdrop and a ModalOverlay defined above it:

const Modal = (props) => (
  <>
    {ReactDOM.createPortal(
      <Backdrop onClick={props.onClick} />,
      document.getElementById("backdrop-root")
    )}
    {ReactDOM.createPortal(
      <ModalOverlay title={props.title} message={props.message} onClick={props.onClick} />,
      document.getElementById("overlay-root")
    )}
  </>
);

Note the fragment again. A component can return two portals only because they sit inside one root. The two features turned out to belong together.

What stuck with me: JSX needs one root, not one element. And where a component is written doesn't have to be where its HTML lives.