react: Unexpected warning when hydrating with portal and SSR

Do you want to request a feature or report a bug?

bug

What is the current behavior?

Given the following (simplified) snippet:

class HoverMenu extends React.Component {
  render() {
    if (typeof document === 'undefined') return null
    const root = document.getElementById('root')
    return ReactDOM.createPortal(<div>Hello World</div>, root)
  }
}

class Para extends React.Component {
  render() {
    return (
      <span>
        Some Text
        <HoverMenu />
      </span>
    )
  }
} 

where div#root is a valid div that exists, the following error is shown when hydrating after SSR:

Warning: Expected server HTML to contain a matching <div> in <span>

The warning goes away if I update the definition of HoverMenu to:

class HoverMenu extends React.Component {
  componentDidMount() {
    this.setState({ isActive: true })
  }
  render() {
    const { isActive} = this.state
    if (!isActive) return null
    const root = document.getElementById('root')
    return ReactDOM.createPortal(<div>Hello World</div>, root)
  }
}

I’d prefer not to do that because of the double rendering caused by setState in componentDidMount.

I don’t quite understand what that error is telling me. No <div /> is rendered server-side in either case. The error is particularly confusing, as the HoverMenu DOM div is not even rendered inside a DOM span. (I wonder if this is happening because HoverMenu is nested inside a React span.)

What is the expected behavior?

No error is thrown. Or, at least that the error message is clearer.

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

Chrome 65 React 16.2 (SSR through Next 5.1)

About this issue

  • Original URL
  • State: open
  • Created 6 years ago
  • Reactions: 14
  • Comments: 28 (3 by maintainers)

Commits related to this issue

Most upvoted comments

While hydrating portals is not supported (https://github.com/facebook/react/issues/13097), the message itself doesn’t make sense. We’ll need to investigate and fix it.

used for things like modals

That’s one use case but there are also others like sidebars and similar which are not necessarily client-only.

they also take a dom element

Right — in the current design. It could change. https://github.com/facebook/react/pull/8386#issuecomment-262375265

i just also don’t see anything valuable from throwing.

The value of throwing is to explicitly acknowledge that portal won’t work. You can easily work around it with {domNode && ReactDOM.createPortal(stuff, domNode)} or similar. Because you already had to do some kind of checking to determine whether you can get the DOM node — so at this point you should have enough data to choose not to emit the portal.

It doesn’t seem like rendering nothing is best — I don’t see what makes portal contents different that we don’t consider it worth server rendering.

We’ll likely come back to this together with the revamp of server renderer for suspense.

const [isClientSide, setIsClientSide] = useState(false);

  useEffect(() => {
    // this useEffect prevents showing Hydration Error
    if (!isClientSide) {
      setIsClientSide(true);
    }
  }, []);
.
.
.

return (
    <>
      {typeof document !== "undefined" &&
        isClientSide &&
        createPortal(
          <div>
            -- Your JSX code
          </div>,
          document.body
        )}
    </>
  );


Hello, is anyone working on this issue? If not I would like to take it.