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?
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."
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
I was trying to convert my class-based component to function based component, which I wrote some while when I was learning REACT, while converting this, I got an error that isOpen is not the function which I kinda dint get as I defined it as a state and called in handleToggle(), which is then being called at the logo of my component.
import React, { useState, useEffect } from "react";
import logo from "../images/logo.svg";
import { FaAlignRight } from "react-icons/fa";
import { Link } from "react-router-dom";
import Badge from '#mui/material/Badge';
import Menu from '#mui/material/Menu';
import MenuItem from '#mui/material/MenuItem';
export default function Navbar(){
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const [isOpen, setIsOpen] = useState(null);
useEffect(() =>{
handleToggle();
});
// state = {
// isOpen: false,
// };
const handleToggle = () => {
setIsOpen(isOpen() );
};
return (
<nav className="navbar">
<div className="nav-center">
<div className="nav-header">
<Link to="/">
<img src={logo} alt="Beach Resort" />
</Link>
<button
type="button"
className="nav-btn"
onClick={handleToggle}
>
<FaAlignRight className="nav-icon" />
</button>
</div>
<ul
className={isOpen ? "nav-links show-nav" : "nav-links"}
>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/rooms">Rooms</Link>
</li>
</ul>
<Badge badgeContent={4} color="primary"
id="basic-button"
aria-controls={open ? 'basic-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
onClick={handleClick}
>
<i className="fa-solid fa-cart-shopping text-light"
style={{ fontSize: 25, cursor: "pointer" }}
></i>
</Badge>
</div>
<Menu
id="basic-menu"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
MenuListProps={{
'aria-labelledby': 'basic-button',
}}
>
<MenuItem onClick={handleClose}>Profile</MenuItem>
<MenuItem onClick={handleClose}>My account</MenuItem>
<MenuItem onClick={handleClose}>Logout</MenuItem>
</Menu>
</nav>
);
}
EVERY PIECE OF ADVICE WILL BE APPRECIATED
useState returns an array with two things: a value that is stored in state, and a function to update it. If you call const [isOpen, setIsOpen] = useState(null), isOpen is your value (originally set as null) and setIsOpen is a function to update it.
When you write const handleToggle = () => { setIsOpen(isOpen() ) }, you're trying to call a null value, which is impossible because it's not a function. That's what the error message is telling you.
Given you want to toggle the value for isOpen, what you should do instead is declare isOpen as a boolean, and call setIsOpen with the opposite of isOpen:
const [isOpen, setIsOpen] = useState(false); // <= set this originally to false
const handleToggle = () => {
setIsOpen(!isOpen); // <= this will set isOpen as true when it is false, and false when it is true
};
However, if you call handleToggle inside a useEffect with no dependency array, like you're doing, it will be called every time there is a rerender, which is probably not what you want. You most likely want to call this in response to a user interaction - so in response to an HTML element event (like onClick). Otherwise you should refactor your code to add the necessary dependencies to useEffect.
isOpen contains value, not a function,try this setIsOpen(p => !p)
How can I Change styles on hover of an appropriate item react? Now the styles are changed of all of the items at a time. Hovering on add to cart button I need to change only the chosen div styles.
https://codesandbox.io/s/trusting-moon-djocul?file=/src/components/Category.js**
import React, { useState } from "react";
import styles from "./category.module.css";
import Categories from "./Categories";
const Category = () => {
const [hovered, setHovered] = useState(false);
const [data, setData] = useState(Categories);
const style = hovered
? { backgroundColor: "#ffcbcb", color: "#fff", transition: "all 0.5s ease" }
: {};
const filterResult = (catItem) => {
const result = Categories.filter((curData) => curData.category === catItem);
setData(result);
};
return (
<>
<div className={styles.items}>
{data.map((value) => {
const { id, title, price, description } = value;
return (
<>
<div className={styles.item} key={id} style={style}>
<h1>{title}</h1>
<p>
{price} <span>$</span>
</p>
<p>{description}</p>
<button
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className={styles.btn}
>
Add to Cart
</button>
</div>
</>
);
};
export default Category;
That's because you have a single "hovered" state shared between all your cards, you should create a "Card" component and have that component have its own hovered state
return (
<>
<div className={styles.items}>
{data.map((value) => {
return (
<>
<Card {...props} />
</>
);
Problem
This issue is occurring cause you are applying CSS to all the cards. The only small thing you are missing in your logic is adding CSS to the only card whose button was being hovered.
Solution
That can be achieved by using event objects on mouse enter and mouse leave events.
<div className={styles.item} key={id} style={style}>
<h1>{title}</h1>
<p>
{price} <span>$</span>
</p>
<p>{description}</p>
<button
onMouseEnter={(e) =>
e.currentTarget.parentElement.classList.add("active_card")
}
onMouseLeave={(e) =>
e.currentTarget.parentElement.classList.remove("active_card")
}
className={styles.btn}
>
Add to Cart
</button>
</div>
This will add a class of "active_card" on the card whose Add To Card button is being hovered, then you can apply your desired styling to that class.
.active_card {
background-color: #ffcbcb !important;
}
Example
Working Code Sandbox Example
I am encountering an error after a git merge (I accidently had eslint warnings turned off). However, I don't know what is exactly causing this error, and how I can fix it.
The error is:
ERROR in [eslint] src/pages/PageHome.js Line 47:49: React Hook
"useState" is called conditionally. React Hooks must be called in the
exact same order in every component render react-hooks/rules-of-hooks
Search for the keywords to learn more about each error.
export default function PageHome() {
var maxWebshopId = 0;
const importedWebshopObject = JSON.parse(
sessionStorage.getItem("client_shops")
);
function handleWindowSizeChange() {
setWidth(window.innerWidth);
}
useEffect(() => {
window.addEventListener("resize", handleWindowSizeChange);
return () => {
window.removeEventListener("resize", handleWindowSizeChange);
};
}, []);
PrimeReact.ripple = true;
const [width, setWidth] = useState(window.innerWidth);
const isMobile = width <= 768;
// Menu fold in/out state
const [visible, setVisible] = useState(true);
if (importedWebshopObject.length >= 1) {
importedWebshopObject.forEach((element) => {
console.log(element);
if (parseInt(element.id) > maxWebshopId) {
maxWebshopId = parseInt(element.id);
}
});
const [WebshopOrderId, setWebshopOrderId] = useState(maxWebshopId);
const value = { WebshopOrderId, setWebshopOrderId };
return(
<WebshopOrderIdContext.Provider value={value}>
<div
className='page-home'
style={{ backgroundColor: "var(--surface-ground)" }}>
<DashboardSidebar
overlay={isMobile}
visible={[visible, setVisible]}
/>
<Menubar
className='border-none pr-2 shadow-1 fixed z-5 w-full p-0'
model={null}
start={
!visible && (
<Button
icon='pi pi-bars'
className='p-button-secondary p-button-text p-3'
onClick={(e) => setVisible(true)}
/>
)
}
end={
<div className='flex flex-row'>
<WebshopSwitcher />
<Divider className='m-0' layout='vertical' />
<LogoutButton />
<Divider className='m-0 mr-2' layout='vertical' />
<DarkModeButton />
</div>
}
/>
<div
id='content-container'
className='flex flex-row justify-content-center transition-duration-300'
style={{
marginLeft: visible && !isMobile ? "20rem" : "0rem",
minHeight: "100vh",
}}>
<span className='spaced-container mt-6 w-full p-5'>
<Outlet />
</span>
</div>
</div>
</WebshopOrderIdContext.Provider>
);
}
else {
return(
<div className='page-home'>
<div className='flex flex-column flex-wrap justify-content-start align-items-start'>
<h1 className='text-3xl font-bold m-6'>
Je moet minstens één webshop op je account hebben om dit portaal te
kunnen gebruiken.
</h1>
<Button
label='terug naar de inlogpagina'
className='p-button-primary p-button-outlined p-3 m-6'
onClick={() => (window.location.href = "/login")}
/>
</div>
</div>
);
}
}
Exactly what the error is telling you. This is inside of an if block:
const [WebshopOrderId, setWebshopOrderId] = useState(maxWebshopId);
React hooks don't allow that. Each rendering of the component must call the same hooks in the same order. Move it outside of the if block, up to where your other useState call is:
const [WebshopOrderId, setWebshopOrderId] = useState(maxWebshopId);
const [width, setWidth] = useState(window.innerWidth);
const isMobile = width <= 768;
Even if the component will only actually use that state based on a condition, the state still needs to exist regardless of that condition so the framework can manage it.
Im trying to make a Modal and when someone clicks to open it I want to disable scrollbar.
My Modal is a component and I cant pass the prop "open" to the condition. When someone clicks to open the Modal the condition doesn't work and the scrollball stays.
My Dialog.js is where I have my array and my functions, I pass them as props to the others components, to each individual Modal.
Dialog.js
export default function Dialog() {
let [Dentisteria, setDentisteria] = useState(false);
let [Endodontia, setEndodontia] = useState(false);
let [Ortodontia, setOrtodontia] = useState(false);
const dataEspecialidades = [
{
setOpen: setDentisteria,
open: Dentisteria,
},
{
setOpen: setEndodontia,
open: Endodontia,
},
{
id: 3,
setOpen: setOrtodontia,
open: Ortodontia,
},
];
return (
<>
<div className="grid gap-8 mx-auto md:grid-cols-3">
{dataEspecialidades.map((item) => {
return (
<>
<Card setOpen={item.setOpen}>
<CardTitle>{item.title}</CardTitle>
<CardDescription>{item.text}</CardDescription>
</Card>
<Modal setOpen={item.setOpen} open={item.open}>
<ModalTitle>{item.title}</ModalTitle>
<ModalDescription>
{item}
</ModalDescription>
</Modal>
</>
);
})}
</div>
</>
);
}
My Card component is used to open the Modal and its working. I pass the prop
setOpen that I have in my Dialog.js.
Card.js
export function Card({ setOpen, children }) {
return (
<>
<div
onClick={() => setOpen(true)}
className="px-4 py-6 text-center rounded-lg cursor-pointer select-none bg-gradient-to-br from-white to-neutral-50 drop-shadow-lg"
>
{children}
</div>
</>
);
}
My Modal component is used to show and close the Modal and its working. I pass the prop setOpen and open that I have in my Dialog.js.
But the open prop is not working in the condition to hide the scrollbar when the Modal is open.
Modal.js
export function Modal({ open, setOpen, children }) {
if (typeof document !== "undefined") {
if (open) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
}
return (
<>
<div
className={`${open ? "" : "hidden"} fixed z-10 inset-0 overflow-y-auto`}
>
<div className="flex items-center justify-center min-h-screen p-4">
<div className="fixed inset-0 bg-black opacity-30"></div>
<div className="relative w-full max-w-2xl p-8 mx-auto bg-white rounded-lg">
{children}
</div>
</div>
</div>
</>
);
}
You are not tracking open with a state, you could use the useEffect hook for this
https://reactjs.org/docs/hooks-effect.html
const [modalIsOpen, setmodalIsOpen] = useState(open);
useEffect(() => {
// Update the body style when the modalIsOpenState changes
if (modalIsOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
}, [modalIsOpen]); // adding this will run useEffect any time modalIsOpen changes see the "Tip: Optimizing Performance by Skipping Effects" part of the documentation for more details
I realise your question is for next.js. I'm used to using React myself, you can use my answer in your Next.js application by importing useEffect like this
import React, { useState, useEffect } from 'react'
when I click the menu item the page loads but the menu stays open.
this is the Mobile Menu it JSX:
const MobileMenu = () => {
const navigation = [
{ link: '/applications', text: 'Applications' },
{ link: '/multi-media', text: 'Media' },
{ link: '/websites', text: 'Websites' },
{ link: '/mobile-apps', text: 'Mobile' },
{ link: '/support', text: 'Support' },
{ link: '/contact', text: 'Contact' },
{ link: '/', text: 'Login' },
];
return (
<NavMenuContainer>
<NavList>
<div className="flex pb-4 lg:px-6 lg:hidden">
<Searchbar id="mobile-search" />
</div>
{navigation.map(nav => (
<NavLink key={nav.text}>
<Link href={nav.link}><a>
{nav.text}
</a></Link>
</NavLink>
))}
</NavList>
</NavMenuContainer>
);
};
export default MobileMenu
this is NAV the MobileMenu menu is in(JSX:
export default function HamburgerMenu(props) {
const [isOpen, setOpen] = useState(false);
//change
const toggleMenu = () => {
let dd = document.body;
dd.classList.toggle("navbar-mobile");
setOpen(!isOpen);
};
return (
<Theburger>
<HamburgerMenuContainer>
<MenuToggle toggle={toggleMenu} isOpen={isOpen} />
<MenuContainer
initial={false}
animate={isOpen ? "open" : "closed"}
variants={menuVariants}
transition={menuTransition}
>
<ContentContainer>
<MobileMenu isOpen={isOpen} />
</ContentContainer>
</MenuContainer>
</HamburgerMenuContainer>
</Theburger>
);
}
this is the website main menu it TSX:
const Navbar: FC<NavbarProps> = ({ links }) => (
<NavbarRoot>
<Container>
<div className={s.nav}>
<div className="flex items-center">
<Link href="/"><a>
<div className="logo">
<Image width={106} height={27} src="/logo.svg" alt="brand" />
</div>
</a></Link>
<nav className={s.navMenu}>
{[...links3 ].map((page) => (
<span key={page.url}>
<Link href={page.url!}>
<a className={s.link}>
{page.name}
</a>
</Link>
</span>
))}
{links?.map((l) => (
<Link href={l.href} key={l.href}>
<a className={s.link}>{l.label}</a>
</Link>
))}
</nav>
</div>
<div className="flex items-center justify-end flex-1 space-x-8 mr-5">
<UserNav />
</div>
<div className="flex items-center justify-end flex-2">
<Nav />
</div>
</div>
</Container>
</NavbarRoot>
)
export default Navbar
Its a nextjs app Im using a Layout component in _app.tsx not sure if that matters but it really shouldn't, I Missed tsx with jsx and according to the docs at NextJS and javascript in general mixing them shouldn't cause problems.
You're missing to give the state as a prop to your toggle menu function.
Thus isOpen is always false and the state gets changed always to true.
Change your toggleMenu() to toggleMenu(isOpen) and it's fine.
export default function HamburgerMenu(props) {
const [isOpen, setOpen] = useState(false);
//change
const toggleMenu = (myState) => {
let dd = document.body;
dd.classList.toggle("navbar-mobile");
setOpen(!isOpen);
};
return (
<Theburger>
<HamburgerMenuContainer>
<MenuToggle toggle={()=>toggleMenu(isOpen)} isOpen={isOpen} />
<MenuContainer
initial={false}
animate={isOpen ? "open" : "closed"}
variants={menuVariants}
transition={menuTransition}
>
<ContentContainer>
<MobileMenu isOpen={isOpen} />
</ContentContainer>
</MenuContainer>
</HamburgerMenuContainer>
</Theburger>
);
}
The reason you're running into the menu being always visible, is because when react compiles the function, the initial value is used whether it was changed or not. Resulting in the following function to always console log "initial" even when the state has changed.
function A() {
const [textState, setTextState] = useState('initial');
const printState = () => {
console.log(textState);
setTextState('changed');
}
return <button onClick={()=>printState()}>Print me</button>
}
this behaves different in the following two scenarios where either the state is from the parent or the props are given to the function.
Parent Props
function B({textState, setTextState}) {
const printState = () => {
console.log(textState);
setTextState('changed');
}
return <button onClick={()=>printState()}>Print me</button>
}
In function B the printState function is given as a prop and the function is rerendered when the props change, causing also the printState function to be compiled again with the new props causing to console log changed instead of initial.
The other option is handling the state in the component itself and giving the state as a prop to our function.
function C() {
const [textState, setTextState] = useState('initial');
const printState = (prop) => {
console.log(prop);
setTextState('changed');
}
return <button onClick={()=>printState(textState)}>Print me</button>
}
Here the prop is given directly to the printState function, while the printState function is not being recompiled the updated state is given as a prop and handled accordingly