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.