Home > Back-end >  How to clone/copy events in react.js
How to clone/copy events in react.js

Time:11-19

I have created a component which generates a Modal Dialog. As you may know, modal must be placed inside root (body) element as a child to defuse any parent styles.

To accomplish the process above, I use vanilla js to clone my Modal component and append it to body like so:

  useEffect(() => {
    const modalInstance = document.getElementById('modal-instance-'   id);

    if (modalInstance) {
      const modal = modalInstance.cloneNode(true);
      modal.id = 'modal-'   id;

      const backdrop = document.createElement('div');
      backdrop.id = 'modal-backdrop';
      backdrop.className = 'hidden fixed top-0 bottom-0 start-0 end-0 bg-black bg-opacity-75 z-[59]';
      backdrop.addEventListener('click', toggleModal);

      document.body.appendChild(backdrop);
      document.body.appendChild(modal);

      const closeBtn = document.querySelector(`#modal-${id} > [data-close='modal']`);
      closeBtn.addEventListener('click', toggleModal);
    }

So far so good and Modal works perfectly; but problems start showing up when I pass elements with events as children to my Modal component.

<Modal id='someId' size='lg' show={showModal} setShow={setShowModal} title='some title'>
  <ModalBody>
    Hellowwww...
    <Button onClick={() => alert('working')} type='button'>test</Button>
  </ModalBody>
</Modal>

The above button has an onClick event that must be cloned when I clone the entire modal and append it to body.

TL;DR

Is there any other way to accomplish the same mechanism without vanilla js? If not, how can I resolve the problem?

CodePudding user response:

You should use the createPortal API from ReactDom https://beta.reactjs.org/apis/react-dom/createPortal

function Modal (props) {
   const wrapperRef = useRef<HTMLDivElement>(null);

   useIsomorphicEffect(() => {
      wrapperRef.current = document.getElementById(/* id of element */)
   }, []) 
  
   return createPortal(<div>/* Modal content */ </div>, wrapperRef )

}

The useIsomorphic effect hook is export const useIsomorphicEffect = typeof document !== 'undefined' ? useLayoutEffect : useEffect;

Because of " Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format."

CodePudding user response:

After spending some time searching a way to resolve this, I found out that the whole process that I went through was wrong and apparently there's a more robust way to accomplish this.

So, here is my final working component in case you need to learn or use it in your projects:

import {useEffect, useState} from 'react';
import Button from '@/components/Button';
import {X} from 'react-bootstrap-icons';
import {createPortal} from 'react-dom';

export const Modal = ({id, title, className = '', size = 'md', show = false, setShow, children}) => {
  const [domReady, setDomReady] = useState(false);

  const sizeClass = {
    sm: 'top-28 bottom-28 start-2 end-2 sm:start-28 sm:end-28 sm:start-60 sm:end-60 xl:top-[7rem] xl:bottom-[7rem] xl:right-[20rem] xl:left-[20rem]',
    md: 'top-16 bottom-16 start-2 end-2 xl:top-[5rem] xl:bottom-[5rem] xl:right-[10rem] xl:left-[10rem]',
    lg: 'top-2 bottom-2 start-2 end-2 sm:top-3 sm:bottom-3 sm:start-3 sm:end-3 md:top-4 md:bottom-4 md:start-4 md:end-4 lg:top-5 lg:bottom-5 lg:start-5 lg:end-5',
  };

  useEffect(() => {
    setDomReady(true);
  }, []);

  return (
    domReady ?
      createPortal(
        <>
          <div className={`${show ? '' : 'hidden '}fixed top-0 bottom-0 start-0 end-0 bg-black bg-opacity-75 z-[59]`} onClick={() => setShow(false)}/>
          <div id={id}
               className={`${show ? '' : 'hidden '}fixed ${sizeClass[size]} bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-200 drop-shadow-lg rounded-lg z-[60] ${className}`}>
            <Button
              className='absolute top-3 end-3'
              type='button'
              size='sm'
              color='secondaryOutlined'
              onClick={() => setShow(false)}
            ><X className='text-xl'/></Button>

            {title && <div className='absolute top-4 start-3 end-16 font-bold'>{title}</div>}
            <div>{children}</div>
          </div>
        </>
        , document.getElementById('modal-container'))
      : null
  );
};

export const ModalBody = ({className = '', children}) => {
  return (
    <div className={`mt-10 p-3 ${className}`}>
      <div className='border-t border-gray-200 dark:border-gray-600 pt-3'>
        {children}
      </div>
    </div>
  );
};

Usage:

_app.js:

<Html>
  <Head/>
  <body className='antialiased' dir='rtl'>
  <Main/>
  <div id='modal-container'/> <!-- Pay attention to this --!>
  <NextScript/>
  </body>
</Html>

Anywhere you need modal:

<Modal id='someId' size='lg' show={showModal} setShow={setShowModal} title='Some title'>
  <ModalBody>
    Hellowwww...
    <Button onClick={() => alert('working')} type='button'>Test</Button>
  </ModalBody>
</Modal>

I should mention that I use tailwindcss library to style my modal and react-bootstrap-icons for icons.

  • Related