Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EuiBottomBar] document is not defined when using server rendering #5656

Closed
smith opened this issue Feb 22, 2022 · 5 comments · Fixed by #6055
Closed

[EuiBottomBar] document is not defined when using server rendering #5656

smith opened this issue Feb 22, 2022 · 5 comments · Fixed by #6055
Assignees

Comments

@smith
Copy link
Contributor

smith commented Feb 22, 2022

When attempting to use EuiBottomBar in a Remix app, the following error occurs when rendering on the server:

ReferenceError: document is not defined
    at new EuiPortal (/Users/smith/Code/remix-iowajs-slides/node_modules/@elastic/eui/lib/components/portal/portal.js:65:24)
    at processChild (/Users/smith/Code/remix-iowajs-slides/node_modules/react-dom/cjs/react-dom-server.node.development.js:3305:14)
    at resolve (/Users/smith/Code/remix-iowajs-slides/node_modules/react-dom/cjs/react-dom-server.node.development.js:3270:5)
    at ReactDOMServerRenderer.render (/Users/smith/Code/remix-iowajs-slides/node_modules/react-dom/cjs/react-dom-server.node.development.js:3753:22)
    at ReactDOMServerRenderer.read (/Users/smith/Code/remix-iowajs-slides/node_modules/react-dom/cjs/react-dom-server.node.development.js:3690:29)
    at renderToString (/Users/smith/Code/remix-iowajs-slides/node_modules/react-dom/cjs/react-dom-server.node.development.js:4298:27)
    at handleRequest (/Users/smith/Code/remix-iowajs-slides/server/build/index.js:389:51)
    at renderDocumentRequest (/Users/smith/Code/remix-iowajs-slides/node_modules/@remix-run/server-runtime/server.js:404:18)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async requestHandler (/Users/smith/Code/remix-iowajs-slides/node_modules/@remix-run/server-runtime/server.js:55:20)

The error message looks similar to #4807.

@chandlerprall
Copy link
Contributor

Two ways to solve; I started by modifying the existing EuiPortal class component to create the div after mounting, storing it in state. Then I converted the component to a function component to match our preference, but because we care about the insert prop only on creation but need to respect portalRef changing during the component lifecycle, I used two useEffects to split the mount & unmount logic around those concerns.

@thompsongl @constancecchen I'll open a PR with whichever approach you like better

Class component
export class EuiPortal extends Component<EuiPortalProps, EuiPortalState> {
  state: EuiPortalState = {
    portalNode: undefined,
  };

  componentDidMount() {
    const { insert } = this.props;

    const portalNode = document.createElement('div');
    this.setState({ portalNode });

    if (insert == null) {
      // no insertion defined, append to body
      document.body.appendChild(portalNode);
    } else {
      // inserting before or after an element
      const { sibling, position } = insert;
      sibling.insertAdjacentElement(insertPositions[position], portalNode);
    }
  }

  componentWillUnmount() {
    const { portalNode } = this.state;
    if (portalNode && portalNode.parentNode) {
      portalNode.parentNode.removeChild(portalNode);
    }
    this.updatePortalRef(null);
  }

  updatePortalRef(ref: HTMLDivElement | null) {
    if (this.props.portalRef) {
      this.props.portalRef(ref);
    }
  }

  render() {
    const { portalNode } = this.state;
    return portalNode == null
      ? null
      : createPortal(this.props.children, portalNode);
  }
}
Function component
export const EuiPortal: React.FC<EuiPortalProps> = ({
  insert,
  portalRef,
  children,
}) => {
  const [portalNode, setPortalNode] = useState<HTMLDivElement>();

  // mount
  useEffect(() => {
    const portalNode = document.createElement('div');
    setPortalNode(portalNode);

    if (insert == null) {
      // no insertion defined, append to body
      document.body.appendChild(portalNode);
    } else {
      // inserting before or after an element
      const { sibling, position } = insert;
      sibling.insertAdjacentElement(insertPositions[position], portalNode);
    }

    portalRef?.(portalNode);
  }, []);

  // unmount
  useEffect(() => {
    return () => {
      if (portalNode && portalNode.parentNode) {
        portalNode.parentNode.removeChild(portalNode);
      }
      portalRef?.(null);
    };
  }, [portalRef, portalNode]);

  return portalNode == null ? null : createPortal(children, portalNode);
};

@thompsongl
Copy link
Contributor

Function component 🙌

@cee-chen
Copy link
Member

++ to the function component! Just curious, does mount and unmount need to be 2 separate useEffects? Is there a specific reason we can't return the unmount callback in the first useEffect?

  useEffect(() => {
    // mount
    const portalNode = document.createElement('div');
    setPortalNode(portalNode);

    if (insert == null) {
      // no insertion defined, append to body
      document.body.appendChild(portalNode);
    } else {
      // inserting before or after an element
      const { sibling, position } = insert;
      sibling.insertAdjacentElement(insertPositions[position], portalNode);
    }

    portalRef?.(portalNode);

    // unmount
    return () => {
      if (portalNode && portalNode.parentNode) {
        portalNode.parentNode.removeChild(portalNode);
      }
      portalRef?.(null);
    };
  }, []);

@pugnascotia
Copy link
Contributor

@chandlerprall did you ever get around to fixing this?

@chandlerprall
Copy link
Contributor

Whoops, did not! I'll add it back to my list for this week.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants