onClick doesn't update Parent's state from Child component - reactjs

I'm trying to create a Modal (popup) that may be closed onClick using a button inside the Modal.js itself. To achieve that I create useState inside Parent.js and pass closeModal function (which updates the Parent's state) into Modal.js via. props;
For whatever reason, onClick event doesn't update the Parent's state (even tho. it manages to fire the closeModal function accepted from props). From console.log I can see that the closeModal function is being run but still Parent.js state doesn't change so the Modal doesn't close. Other events like onMouseDown or onChange do work correctly and Modal get's closed as supposed to.
Could you, please, explain why it doesn't work with onClick and what happens here?
Here the code down below and a sandbox to play with: Sandbox
Parent.js
import { useState } from "react";
import Modal from "./Modal";
export default () => {
const [modal, setModal] = useState({
isShown: false,
name: ""
});
const { isShown } = modal;
const openModal = () => {
setModal({ ...modal, isShown: true });
};
const closeModal = () => {
setModal({ ...modal, isShown: false });
console.log("Modal must be close!");
};
return (
<div className="parent" onClick={openModal}>
{isShown ? <Modal closeModal={closeModal} /> : null}
<div className="message">Open Modal</div>
</div>
);
};
Modal.js
export default ({ closeModal }) => {
return (
<div className="modal">
<button className="close" onClick={closeModal}>
onClick
</button>
<button className="close" onMouseDown={closeModal}>
onMouseDown
</button>
<input type="text" placeholder="onChange" onChange={closeModal} />
</div>
);
};
P.S.: I managed to make that work by moving onClick which opens the modal inside Parent.js, but I still don't understand why it didn't work and what really happens. I assume that with onClick the state gets updated so fast that at the moment it gets compared to the old one it appears there is no difference, so it ends up not updating. But this is just my guess...
Could you clarify for me, please?
Parent.js
import { useState } from "react";
import Modal from "./Modal";
export default () => {
const [modal, setModal] = useState({
isShown: false,
name: ""
});
const { isShown } = modal;
const openModal = () => {
setModal({ ...modal, isShown: true });
};
const closeModal = () => {
setModal({ ...modal, isShown: false });
console.log("Modal must be close!");
};
return (
<div className="parent">
{isShown ? <Modal closeModal={closeModal} /> : null}
<div className="message" onClick={openModal}>Open Modal</div>
</div>
);
};

You need to stop the click event propagation. As Child div is overlapping with Parent (its button), both events are taking place in order:
"close the modal (in child/Modal)"
"open the modal (in parent/Button)"
That's the reason it is staying opened. To fix it, use stopPropagation in the Modal (child) component :
<button className="close" onClick={e => {
e.stopPropagation()
closeModal()
}}>
PS: I would suggest going with this modal

Related

Hide modal on click outside in react hooks

i have a modal component in my react app and i need to close it on click outside
import React from "react";
import ReactDOM from "react-dom";
import style from "./Modal.module.scss";
const Modal = ({ isShowing, hide, childrenContent, childrenHeader }) =>
isShowing
? ReactDOM.createPortal(
<React.Fragment>
<div className={style.modalOverlay} />
<div
className={style.modalWrapper}
aria-modal
aria-hidden
tabIndex={-1}
role="dialog"
>
<div className={style.modal}>
<div className={style.modalHeader}>
{childrenHeader}
<button
type="button"
className={style.modalCloseButton}
data-dismiss="modal"
aria-label="Close"
onClick={hide}
>
<span aria-hidden="true">×</span>
</button>
</div>
{childrenContent}
</div>
</div>
</React.Fragment>,
document.body
)
: null;
export default Modal;
i was try to use this solution but it's not work in my code, how can i fix it?
Just a tip, when looking at the html you can use the native <dialog> tag, this is the semantically correct way to display a dialog type pop-up box, which yours looks to be.
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog
Dialog has a showModal() method, and a .close() method. This would be a better way of displaying a pop-up type dialog, than using <div> tags. It also allows you to use the native HTML5 methods, rather than trying to provide a work around using React.
I would reccomend this method over trying to look for work arounds
const Modal = ({ children, showModal, toggleModal }) => {
const wrapperRef = React.useRef(null);
const closeModal = React.useCallback(
({ target }) => {
if (
wrapperRef &&
wrapperRef.current &&
!wrapperRef.current.contains(target)
) {
toggleModal();
}
},
[toggleModal]
);
React.useEffect(() => {
document.addEventListener("click", closeModal, { capture: true });
return () => {
document.removeEventListener("click", closeModal, { capture: true });
};
}, [closeModal]);
return showModal
? ReactDOM.createPortal(
<>
<div ref={wrapperRef} className="modal">
{children}
</div>
</>,
document.body
)
: null;
};
Modal.propTypes = {
children: PropTypes.node.isRequired,
showModal: PropTypes.bool.isRequired,
toggleModal: PropTypes.func.isRequired
};
export default Modal;
in your parent component :
const Parent = () => {
const [showModal, setModalState] = React.useState(false);
const toggleModal = React.useCallback(() => {
setModalState((prevState) => !prevState);
}, []);
return (
<div>
<Modal showModal={showModal} toggleModal={toggleModal}>
<h1>Hello!</h1>
... some other childrens
<button
onClick={toggleModal}
>
Close
</button>
</Modal>
</div>
);
};

Test modal component within another component

I'm trying to test a component that should open its modal. Modal is a part of this component, but it's rendered with createPortal(). I first check if modal exist in the document and after button click if it appeared but test fails.
Component:
const [openModal, setOpenModal] = useState(false);
function Component() {
return (
<div>
<button onClick={() => setOpenModal(true)}>Open Modal</button>
<Modal open={openModal}/>
</div>
)
}
Modal:
const Modal = ({ open, children }) => {
return createPortal(
<div style={{display: open ? "block" : "none"}} data-testid="modal">
{children}
</div>,
document.getElementById("modals")
);
};
Test:
test("component that opens modal", async () => {
render(<Component />);
const button = screen.getByText("Open Modal");
const modal = screen.queryByTestId("modal");
expect(modal).not.toBeInTheDocument();
fireEvent.click(button);
await waitFor(() => expect(modal).toBeInTheDocument()); // Fails
});
I tried to test it with await waitFor(() => expect(modal).toBeInTheDocument()) and also with standard expect(modal).toBeInTheDocument()). I also tried to render modal without portal, but still had no effect on the test. Could you please explain how it should be tested?
This kind of behavior is probably generating a new render, try using act
Some useful links: https://github.com/threepointone/react-act-examples/blob/master/sync.md
https://testing-library.com/docs/preact-testing-library/api/#act

Avoid re-creating body component on each render of react-modal

I'm using this lib to create a modal
I have 3 components: Table, Modal and List
Table has Modal (a custom React Modal), and the body of Modal will be List.
Now the problem is, List has some functions which change the states of Table, so when I do something that can make Table's state change, Table and Modal will be re-rendered when Modal is re-rendered, it re-creates a new List which leads to lost all stuffs I'm doing with List.
Here is a simple version of my app. link
Now I don't want List to be re-created each time Modal is re-rendered. Is there any way to archive that? (I don't want to create a modal myself or use global state management in this case)
import { useEffect, useMemo, useState } from "react";
import ReactModal from "react-modal";
ReactModal.setAppElement("#root");
const List = ({ onClick }) => {
useEffect(() => {
console.log("List is mounted");
}, []);
return <button onClick={onClick}>Click me!</button>;
};
const Modal = ({ state, body, isOpen }) => {
useEffect(() => {
console.log("Modal is re-rendered");
});
return (
<div
id="react modal wrapper"
style={{
display: `${isOpen ? "block" : "none"}`
}}
>
<ReactModal isOpen={isOpen}>
<div>
state is {state}
<br />
{body}
</div>
</ReactModal>
</div>
);
};
const Table = ({ state, onClick, isOpen }) => {
useEffect(() => {
console.log("Table is re-rendered");
});
const memorizedList = useMemo(() => <List onClick={onClick} />, []);
return (
<div>
state: {state}
<Modal state={state} body={memorizedList} isOpen={isOpen} />
</div>
);
};
const App = () => {
const [state, setState] = useState(1);
const onClick = () => setState((v) => v + 1);
return (
<div>
<button onClick={onClick}>Change state</button>
<Table state={state} onClick={onClick} isOpen={state % 2 === 0} />
</div>
);
};
export default App;

how do control the state for multiple component with one function

I have one simple app that include 3 identical button and when I click the button, onClick event should trigger to display one span. for now, I have use one one state to control span show or not and once I click any one of button all span show. How can I implement the code, so when I click the button, only the correspond span display
import "./styles.css";
import React, { useState } from "react";
const Popup = (props) => {
return <span {...props}>xxx</span>;
};
export default function App() {
const [isOpen, setIsOpen] = useState(true);
const handleOnClick = () => {
setIsOpen(!isOpen);
};
return (
<div className="App">
<button onClick={handleOnClick}> Show popup1</button>
<Popup hidden={isOpen} />
<button onClick={handleOnClick}> Show popup2</button>
<Popup hidden={isOpen} />
<button onClick={handleOnClick}> Show popup3</button>
<Popup hidden={isOpen} />
</div>
);
}
codesandbox:
https://codesandbox.io/s/cocky-fermi-je8lr?file=/src/App.tsx
You should rethink how the components are used.
Since there is a repeating logic and interface, it should be separated to a different component.
const Popup = (props) => {
return <span {...props}>xxx</span>;
};
interface Props {
buttonText: string
popupProps?: any
}
const PopupFC: React.FC<Props> = (props) => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(!isOpen)}>{props.buttonText}</button>
<Popup hidden={isOpen} {...props.popupProps} />
</>
)
}
export default function App() {
const [isOpen, setIsOpen] = useState(true);
const handleOnClick = () => {
setIsOpen(!isOpen);
};
return (
<div className="App">
<PopupFC buttonText="Show popup1" />
<PopupFC buttonText="Show popup2" />
<PopupFC buttonText="Show popup3" />
</div>
);
}
If each Popup needs its own isOpen state, it would not be possible to achieve with a single boolean state.
Perhaps converting both the button and the span to a single component and letting each Popup component handle its own isOpen:
import "./styles.css";
import React, { useState } from "react";
const Popup = (props) => {
const [isOpen, setIsOpen] = useState(true);
const handleOnClick = () => {
setIsOpen(!isOpen);
};
return (
<>
<button onClick={handleOnClick}>{props.children}</button>
{isOpen && <span {...props}>xxx</span>}
</>
);
};
export default function App() {
return (
<div className="App">
<Popup>Show popup 1</Popup>
<Popup>Show popup 2</Popup>
<Popup>Show popup 3</Popup>
</div>
);
}
That happens simply because you are using the same state "isOpen" for all buttons,
once you click any one of them it reflects all buttons because it's the same value.
you could solve this using Custom Hook since you repeat the logic or you could separate them into small components
Based on your comment, you only want one popup to be open at a time. That was not clear in your original question so the other answers don't address this.
Right now you are just storing a value of isOpen that is true or false. That is not enough information. How do you know which popup is open?
If you want to show just one at a time, you can instead store the number or name (any sort of unique id) for the popup which is currently open.
We make the Popup a "controlled component" where instead of managing its own internal isOpen state, it receives and updates that information via props.
The App component is responsible for managing which popup is open and passing the right props to each Popup component. Since we are doing the same thing for multiple popups, I moved that logic into a renderPopup helper function.
Popup
interface PopupProps {
isOpen: boolean;
open: () => void;
close: () => void;
label: string;
}
const Popup = ({ isOpen, open, close, label }: PopupProps) => {
return (
<>
<button onClick={open}> Show {label}</button>
{isOpen && (
<div>
<h1>{label}</h1>
<span>xxx</span>
<button onClick={close}>Close</button>
</div>
)}
</>
);
};
App
export default function App() {
// store the label of the popup which is open,
// or `null` if all are closed
const [openId, setOpenId] = useState<string | null>(null);
const renderPopup = (label: string) => {
return (
<Popup
label={label}
isOpen={openId === label} // check if this popup is the one that's open
open={() => setOpenId(label)} // open by setting the `openId` to this label
close={() => setOpenId(null)} // calling `close` closes all
/>
);
};
return (
<div className="App">
{renderPopup("Popup 1")}
{renderPopup("Popup 2")}
{renderPopup("Popup 3")}
</div>
);
}
Code Sandbox

setState hook does't change state invoking from child

I am using hook in component to manage modal state.
(Class version of component reproduce the problem)
handleClick will open modal and handleModalClose should close.
I send handleModalClose to Modal component and with console.log could see, that it is processed, but the isModalOpen state not changed (the same for callback setState).
When I am trying to invoke it with setTimeout - state changes and Modal is closing.
Why the hell the state not changes when I invoke changing from child???
const [isModalOpen, setModalOpen] = useState(false);
const handleClick = () => {
setModalOpen(true);
// setTimeout(() => handleModalClose, 10000);
};
const handleModalClose = () => {
console.log('!!!!!!!!handleModalClose');
setModalOpen(false);
};
return (
<div onClick={handleClick}>
{isModalOpen && <Modal closeModal={handleModalClose} />}
</div>
);
and here is extract from Modal
const Modal = (props) => {
const { closeModal } = props;
return (
<>
<div className="modal">
<form className="" onSubmit={handleSubmit(onSubmit)}>
<button type="button" className="button_grey button_cancel_modal" onClick={closeModal}>
</button>
PROBLEM SOLVED. e.stopPropagation() - added.
const handleModalClose = (e) => {
e.stopPropagation();
console.log('!!!!!!!!handleModalClose');
setModalOpen(false);
};
Modal was closed and instantly reopen by bubbling w/o this.

Resources