react-portal icon indicating copy to clipboard operation
react-portal copied to clipboard

Does not work with SSR

Open garrettmaring opened this issue 5 years ago • 11 comments

When running this code with SSR I am seeing this:

Invariant Violation: Portals are not currently │ supported by the server renderer. Render them conditionally so that they only appear on the client render.

React 16.3

garrettmaring avatar Nov 05 '18 21:11 garrettmaring

Also don't love seeing Warning: Expected server HTML to contain a matching <div> in <div>.

michaeljonathanblack avatar Nov 09 '18 20:11 michaeljonathanblack

I don't currently use this lib with SSR. PRs appreciated!

tajo avatar Nov 27 '18 00:11 tajo

Weird to see the OP having an error with SSR, it doesn't seem like a portal should render on the server at all due to the canUseDOM check, here:

https://github.com/tajo/react-portal/blob/master/src/Portal.js#L15

It does make sense that we're seeing the mismatch server-to-client, however. I wonder if returning an empty <div /> would resolve the mismatch, or if there's a better way to handle expected server-client mismatches.

michaeljonathanblack avatar Nov 27 '18 19:11 michaeljonathanblack

Let's add a state variable and make it true on componentDidMount. Call createPortal if the state variable is true.

This will help to support SSR.

vishalvijay avatar Apr 13 '19 21:04 vishalvijay

I think I understand why I am seeing this.

I server-side render my React app. However, I use several libraries that unsafely reference window. Without any clever code-splitting in place yet, that meant I needed to stub out the global window. I think that's what's causing the canUseDOM check to incorrectly return true.

I need to keep this at the moment but I'm wondering if it'd be straightforward to expose an optional property on the component that will be used in place of canUseDOM if provided.

Maybe a function property isSSR which returns a bool. That'd work in my case but not sure if this is very common.

garrettmaring avatar Apr 16 '19 22:04 garrettmaring

That makes sense; would be easy enough and is backward compatible. Could throw the rely-on-state fix that @vishalvijay mentioned in there, too. 💃

michaeljonathanblack avatar Apr 16 '19 22:04 michaeljonathanblack

I server-side render my React app. However, I use several libraries that unsafely reference window. Without any clever code-splitting in place yet, that meant I needed to stub out the global window. I think that's what's causing the canUseDOM check to incorrectly return true.

Do you have to stub-out even window.document.createElement? Can you apply stub-outs only in places with those unsafe libs? Can you wrap this lib with your own isSSR check?

There are many other options than adding additional API to the Portal component.

Could throw the rely-on-state fix that @vishalvijay mentioned in there, too. 💃

That would cause re-rendering.

tajo avatar Apr 16 '19 23:04 tajo

Do you have to stub-out even window.document.createElement?

I might not need to though I'll need to check.

Can you apply stub-outs only in places with those unsafe libs?

I'm not sure what you mean here. So I have a Portal which renders a component that uses the DOM. Inside of that component, the libs are imported. I'll need to stub out window.document before that code path is reached. It's a good point that I could likely be more precise about where the stub is applied but given that it's hoisted dependencies, the call has been to put it at the top/global level.

Can you wrap this lib with your own isSSR check?

Do you mean fork it and implement a new isSSR? I certainly could do that and it'd be easy enough.

I suppose the support should only be added to react-portal if there is significant traction on this issue. I'm also happy to make a PR for this, it seems straightforward enough.

garrettmaring avatar Apr 18 '19 20:04 garrettmaring

So I have a Portal which renders a component that uses the DOM. Inside of that component, the libs are imported. I'll need to stub out window.document before that code path is reached.

Component should manipulate the DOM only in componentDidMount or useEffect, so that code should never be reached on the server (and no stub needed).

Or does it touch the DOM once you import it (causing an import side-effect)? That's annoying but you could probably use dynamic import to still load it in componentDidMount / useEffect.

My point: There is no good reason why you should stub out window in your React application during the SSR. It's better to avoid import side-effects or use dynamic imports.

tajo avatar Apr 18 '19 21:04 tajo

It was already suggested above, but this was my solution to this:

  1. Use some kind of state variable (I used hooks for this) to determine if your component using react-portal has mounted.
  2. If the state variable shows that the component has not mounted, return null. Otherwise, return your component as you normally would.

Here's some code:

import React, { useState, useEffect } from "react";
import { Portal } from "react-portal";

const MyComponent = props => {
  const [hasMounted, setHasMounted] = useState(false);

  // Will be called on initial mount.
  useEffect(() => {
    setHasMounted(true);
  }, []);

  // Note:
  // This check is necessary for this component to work when used with SSR.
  // While react-portal will itself check if window is available, that is not
  // enough to ensure that there arent discrepancies between what the server
  // renders and what the client renders, as the client *will* have access to
  // the window. Therefore, we should only render the root level portal element
  // once the component has actually mounted, as determined by a state variable.
  if (!hasMounted) {
    return null;
  }

  return (
    <Portal>
      { 
        // ... etc ...
      }
    </Portal>
  );
}

Note that this wont always be required. For example, if your component is included in some other component based on some condition that is only possible given some user interaction (e.g.; a button click that sets some kind of modalIsOpen state to true, and then a boolean check in the render method of that component that conditionally includes your portal component), then you should be fine, as there wont be any difference between what your server sees, and what the client sees on initial render.

However, in my case I am using react-portal for an always-present FlashMessages component that is included in my base Layout component for the entire application.

bradley avatar Aug 29 '19 21:08 bradley

For nextjs users, check their example here:

https://github.com/zeit/next.js/tree/canary/examples/with-portals

Here's the relevant piece:

import React from 'react'
import ReactDOM from 'react-dom'

export class Portal extends React.Component {
  componentDidMount () {
    this.element = document.querySelector(this.props.selector)
    this.forceUpdate()
  }

  render () {
    if (this.element === undefined) {
      return null
    }

    return ReactDOM.createPortal(this.props.children, this.element)
  }
}

Basically return null on ssr and render on client, like suggested above by many others.

In case you do want to render it on server and have the time to experiment, check cheerio out, https://github.com/MichalZalecki/react-portal-universal/ has an example of a concept, basically just mimic the client behavior on server like so:

import * as ReactDOMServer from "react-dom/server";
import { load }  from "cheerio";
import { flushUniversalPortals } from "./index";

export function appendUniversalPortals(html: string) {
  const portals = flushUniversalPortals();
  if (!portals.length) {
    return html;
  }
  const $ = load(html);
  portals.forEach(([children, selector]) => {
    const markup = ReactDOMServer.renderToStaticMarkup(children);
    $(markup).attr("data-react-universal-portal", "").appendTo((selector as any))
  });
  return $.html({ decodeEntities: false });
}

Note that this is just a concept, their lib shouldn't be used in prod since portals will be shared between the requests, if you manage to implement this isolating the requests (which should be possible with next) then you're fine.

fernandobandeira avatar Sep 06 '19 01:09 fernandobandeira