JoeTidee
JoeTidee

Reputation: 26084

Can a React portal be used in a Stateless Functional Component (SFC)?

I have used ReactDOM.createPortal inside the render method of a stateful component like so:

class MyComponent extends Component {
    ...
    render() {
        return (
            <Wrapper>
                {ReactDOM.createPortal(<FOO />, 'dom-location')}
            </Wrapper>
        )
    }
}

... but can it also be used by a stateless (functional) component?

Upvotes: 18

Views: 30280

Answers (9)

Pumuckelo
Pumuckelo

Reputation: 389

IMPORTANT useRef/useState to prevent bugs

It's important that you use useState or useRef to store the element you created via document.createElement because otherwise it gets recreated on every re-render

//This div with id of "overlay-portal" needs to be added to your index.html or for next.js _document.tsx
const modalRoot = document.getElementById("overlay-portal")!;


//we use useRef here to only initialize el once and not recreate it on every rerender, which would cause bugs
  const el = useRef(document.createElement("div"));

  useEffect(() => {
    modalRoot.appendChild(el.current);

    return () => {
      modalRoot.removeChild(el.current);
    };
  }, []);

return ReactDOM.createPortal(
      <div
        onClick={onOutSideClick}
        ref={overlayRef}
        className={classes.overlay}
      >
        <div ref={imageRowRef} className={classes.fullScreenImageRow}>
          {renderImages()}
        </div>
        <button onClick={onClose} className={classes.closeButton}>
          <Image width={25} height={25} src="/app/close-white.svg" />
        </button>
      </div>,
    el.current
  );

Upvotes: 3

Samuel
Samuel

Reputation: 2635

Will chime in with an option where you dont want to manually update your index.html and add extra markup, this snippet will dynamically create a div for you, then insert the children.

export const Portal = ({ children, className = 'root-portal', el = 'div' }) => {
  const [container] = React.useState(() => {
    // This will be executed only on the initial render
    // https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
    return document.createElement(el);
  });

  React.useEffect(() => {
    container.classList.add(className)
    document.body.appendChild(container)
    return () => {
      document.body.removeChild(container)
    }
  }, [])

  return ReactDOM.createPortal(children, container)
}

Upvotes: 54

Wallace Ferreira
Wallace Ferreira

Reputation: 59

Sharing my solution:

// PortalWrapperModal.js

import React, { useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
import $ from 'jquery';

const PortalWrapperModal = ({
  children,
  onHide,
  backdrop = 'static',
  focus = true,
  keyboard = false,
}) => {
const portalRef = useRef(null);

const handleClose = (e) => {
    if (e) e.preventDefault();
    if (portalRef.current) $(portalRef.current).modal('hide');
  };

useEffect(() => {
    if (portalRef.current) {
      $(portalRef.current).modal({ backdrop, focus, keyboard });
      $(portalRef.current).modal('show');
      $(portalRef.current).on('hidden.bs.modal', onHide);
    }
  }, [onHide, backdrop, focus, keyboard]);

return ReactDOM.createPortal(
    <>{children(portalRef, handleClose)}</>,
    document.getElementById('modal-root')
  );
};

export { PortalWrapperModal };

Upvotes: 0

Tom
Tom

Reputation: 2036

TSX version based on @Samuel's answer (React 17, TS 4.1):

// portal.tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom'

interface IProps {
    className? : string
    el? : string
    children : React.ReactNode
}

/**
 * React portal based on https://stackoverflow.com/a/59154364
 * @param children Child elements
 * @param className CSS classname
 * @param el HTML element to create.  default: div
 */
const Portal : React.FC<IProps> = ( { children, className, el = 'div' } : IProps ) => {
    
    const [container] = React.useState(document.createElement(el))
    
    if ( className )
        container.classList.add(className)

    React.useEffect(() => {
        document.body.appendChild(container)
        return () => {
            document.body.removeChild(container)
        }
    }, [])

    return ReactDOM.createPortal(children, container)
}

export default Portal

Upvotes: 7

Blujedis
Blujedis

Reputation: 366

Portal with SSR (NextJS)

If you are trying to use any of the above with SSR (for example NextJS) you may run into difficulty.

The following should get you what you need. This methods allows for passing in an id/selector to use for the portal which can be helpful in some cases, otherwise it creates a default using __ROOT_PORTAL__.

If it can't find the selector then it will create and attach a div.

NOTE: you could also statically add a div and specify a known id in pages/_document.tsx (or .jsx) if again using NextJS. Pass in that id and it will attempt to find and use it.

import { PropsWithChildren, useEffect, useState, useRef } from 'react';
import { createPortal } from 'react-dom';

export interface IPortal {
  selector?: string;
}

const Portal = (props: PropsWithChildren<IPortal>) => {

  props = {
    selector: '__ROOT_PORTAL__',
    ...props
  };

  const { selector, children } = props;

  const ref = useRef<Element>()
  const [mounted, setMounted] = useState(false);

  const selectorPrefixed = '#' + selector.replace(/^#/, '');

  useEffect(() => {

    ref.current = document.querySelector(selectorPrefixed);

    if (!ref.current) {
      const div = document.createElement('div');
      div.setAttribute('id', selector);
      document.body.appendChild(div);
      ref.current = div;
    }

    setMounted(true);

  }, [selector]);

  return mounted ? createPortal(children, ref.current) : null;

};

export default Portal;

Usage

The below is a quickie example of using the portal. It does NOT take into account position etc. Just something simple to show you usage. Sky is limit from there :)

import React, { useState, CSSProperties } from 'react';
import Portal from './path/to/portal'; // Path to above

const modalStyle: CSSProperties = {
  padding: '3rem',
  backgroundColor: '#eee',
  margin: '0 auto',
  width: 400
};

const Home = () => {

  const [visible, setVisible] = useState(false);

  return (
    <>
      <p>Hello World <a href="#" onClick={() => setVisible(true)}>Show Modal</a></p>
      <Portal>
        {visible ? <div style={modalStyle}>Hello Modal! <a href="#" onClick={() => setVisible(false)}>Close</a></div> : null}
      </Portal>
    </>
  );

};

export default Home;

Upvotes: 3

JoeTidee
JoeTidee

Reputation: 26084

It can be done like this for a fixed component:

const MyComponent = () => ReactDOM.createPortal(<FOO/>, 'dom-location')

or, to make the function more flexible, by passing a component prop:

const MyComponent = ({ component }) => ReactDOM.createPortal(component, 'dom-location')

Upvotes: 15

RIYAJ KHAN
RIYAJ KHAN

Reputation: 15290

can it also be used by a stateless (functional) component ?

yes.

const Modal = (props) => {
      const modalRoot = document.getElementById('myEle');

      return ReactDOM.createPortal(props.children, modalRoot,);
    }

Inside render :

render() {
    const modal = this.state.showModal ? (
      <Modal>
        <Hello/>
        </Modal>
    ) : null;

    return (
      <div>
      <div id="myEle">
        </div>
      </div>
    );
  }

Working codesandbox#demo

Upvotes: 7

Efe
Efe

Reputation: 5816

const X = ({ children }) =>  ReactDOM.createPortal(children, 'dom-location')

Upvotes: 1

Denys Kotsur
Denys Kotsur

Reputation: 2599

Yes, according to docs the main requirements are:

The first argument (child) is any renderable React child, such as an element, string, or fragment. The second argument (container) is a DOM element.

In case of stateless component you can pass element via props and render it via portal.

Hope it will helps.

Upvotes: 3

Related Questions