Test modal component within another component - reactjs

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

Related

Axios call made before modal box opens

I'm using bootstrap to open a modal. Previously I was trying to use a boolean to show/ close the modal but couldn't get it to work. Here is athe StackOverflow post where I was trying to get some help:
(How to open Bootstrap Modal from a button click in React)
The code below is making an AJAX request before a user has clicked the button to open the modal to present the user with some data.
This button is in a main page:
<td><button type="button" data-bs-toggle="modal" data-bs-target="#staticBackdrop" onClick={() => rowEvents(id)}>Compare</button></td>
I include this component in the main page which in effect is then making the axios request:
<ComparisonModal previousWon={previousWon} currentWon={currentWon} />
useEffect(() => {
async function fetchData() {
const content = await client.get('1');
}
fetchData();
}, []);
I'm quite new to React, is there a better way to do this so that the Axios call is not made until a user clicks on the button to show the modal please?
Well, you have multiple questions at the same time, I just made this code sandbox as an example: https://codesandbox.io/s/relaxed-benji-8k0fr
Answering your questions:
Yes, you can show modal with a boolean. Basically would be like {booleanCondition && <MyModal />} (It's included in the code sandbox
Yes, you can do the axios request before when clicking the button. In the example we define a function: handleClick and on handleClick we do 2 things, getData which is a mocked request which last 3 seconds and setShowModal to be able to see the modal.
The result would be:
export default function App() {
const [showModal, setShowModal] = React.useState(false);
const [data, setData] = React.useState({});
const getData = async (id) => {
await wait(3000);
setData({ loaded: id });
};
const handleClick = (id) => {
setData({});
setShowModal(true);
getData(1);
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<button onClick={() => handleClick(1)}>Show Modal with Id 1</button>
<button onClick={() => handleClick(2)}>Show Modal with Id 2</button>
{showModal && <Modal data={data} onClose={() => setShowModal(false)} />}
</div>
);
}
I would try set a id to the button as it is a button on a table.
<td><button id="1" type="button" data-bs-toggle="modal" data-bs-target="#staticBackdrop" onClick={() => fetchData(id)}>Compare</button></td>
const fetchData = async (id) => {
try {
const content = await client.get(id);
} catch (err) {
console.log(err);
}
}

React - Manage Bootstrap Modals in components

I have a ReactJS app with 4 screens/components. Each screen can link to another one.
I want to use Modals to display content of each screen, this way I don't lose the state of the current screen.
For now I just set the Modal on my 1st component :
<Modal show={this.state.show}
ref={this.ModalGlobal}
onHide={() => this.setState({show: false})}
>
<Modal.Body>
{this.state.id &&
<MyComponentB id={this.state.id} />
}
</Modal.Body>
</Modal>
On my ComponentB, I want to open the same Modal with different ID.
I tried to use references, but I don't know what to do with that in my ComponentB ?
Like :
this.ModalGlobal.current.destroy
Do I have to use Redux or can it be done using contexts or other solution ?
Instead of having one modal close another one and open that one, would it be possible to instead have the modal update its own contents based on the ID? You could make a wrapper for the modal that will update the body of the modal depending on the current ID. Something like this:
const MyModal = ({id}) => {
const [modalPage, setModalPage] = useState(id);
const [modalIsOpen, setModalIsOpen] = useState(false);
useEffect(() => {
setModalPage(id)
}, [id]);
const openModal = async () => {
setModalIsOpen(true);
document.body.style.overflowY = "hidden";
}
const closeModal = () => {
setModalIsOpen(false);
document.body.style.overflowY = "";
}
const modalPages = {
'welcome': <WelcomeComponent setModalPage />,
'products': <ProductsComponent setModalPage />,
'contact': <ContactComponent setModalPage />
}
const content = modalPages[modalPage];
return (
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
className="react-modal"
overlayClassName="react-modal-overlay"
>
{content}
</Modal>
);
}

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

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

Update child state based on parent state react functional components

Let's say we have a component Accordion that has an internal state isOpen, so you can close and open this component.
We now want to have a parent component that also has a state isOpen and has button. In this component, we have 2 times Accordion and we are passing to Accordion isOpen and we want that if the parent changes state isOpen Accordion accept this.
All component are functional components
const Accordion = ({ isOpen: parentIsOpen = false }) => {
const [isOpen, setIsOpen] = useState(parentIsOpen);
const handleSetIsOpen = () => setIsOpen(!isOpen);
return (
<div>
I'm open: {isOpen}
<button onClick={handleSetIsOpen}>toggle isOpen child</button>
</div>
);
};
const MasterComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const handleSetIsOpen = () => setIsOpen(!isOpen);
return (
<div>
<button onClick={handleSetIsOpen}>toggle isOpen parent</button>
<Accordion isOpen={isOpen} />
<Accordion isOpen={isOpen} />
</div>
);
};
In this case above Accordion will take on first render as the initial state parent isOpen prop. In case we press the button toggle isOpen parent we will change the parent state but children will not be updated.
To fix this we can use useEffect
const Accordion = ({ isOpen: parentIsOpen = false }) => {
const [isOpen, setIsOpen] = useState(parentIsOpen);
const handleSetIsOpen = () => setIsOpen(!isOpen);
useEffect(() => {
if (parentIsOpen !== isOpen) {
setIsOpen(parentIsOpen);
}
}, [parentIsOpen]);
return (
<div>
I'm open: {isOpen}
<button onClick={handleSetIsOpen}>toggle isOpen child</button>
</div>
);
};
const MasterComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const handleSetIsOpen = () => setIsOpen(!isOpen);
return (
<div>
<button onClick={handleSetIsOpen}>toggle isOpen parent</button>
<Accordion isOpen={isOpen} />
<Accordion isOpen={isOpen} />
</div>
);
};
in this case, children will be properly updated when a parent changes isOpen state.
There is one issue with this:
"React Hook useEffect has a missing dependency: 'isOpen'. Either include it or remove the dependency array react-hooks/exhaustive-deps"
So how to remove this issue that esLint is complaining and we do not want to put isOpen in this since it will cause bug.
in case we add isOpen into the array like this:
useEffect(() => {
if (parentIsOpen !== isOpen) {
setIsOpen(parentIsOpen);
}
}, [parentIsOpen, isOpen]);
We will have then a situation where we will click on the internal button in accordion and update the internal state then useEffect will run and see that parent has a different state than the child and will immediately set the old state.
So basically you have a loop where the accordion will never be open then.
The question is what is the best way to update the child state based on the parent state?
Please do not suggest to put all-state in parent and pass props without child state. this will not work since both Accordions in this example have to have their own state and be able to open and close in an independent way, but yet if parent says close or open it should listen to that.
Thank you!
Actually I would say this is way to do it
const Accordion = ({ isOpen: parentIsOpen = false }) => {
const [isOpen, setIsOpen] = useState(parentIsOpen);
const handleSetIsOpen = () => setIsOpen(!isOpen);
useEffect(() => {
setIsOpen(parentIsOpen);
}, [parentIsOpen]);
return (
<div>
I'm open: {isOpen}
<button onClick={handleSetIsOpen}>toggle isOpen child</button>
</div>
);
};
const MasterComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const handleSetIsOpen = () => setIsOpen(!isOpen);
return (
<div>
<button onClick={handleSetIsOpen}>toggle isOpen parent</button>
<Accordion isOpen={isOpen} />
<Accordion isOpen={isOpen} />
</div>
);
};
So just remove state check in a child component, let him update the state but since is updated with the same value it will not rerender or do some expensive behavior.
Tested it today and with a check, if states are different or without is the same, react takes care to not trigger rerender if the state that is updated is the same as before.
What you’re saying not to suggest is in fact the solution I would offer… You’ll need state to control isOpen for the parent component. Also, you should have separate methods in the parent that control state for each accordion, passed along to each accordion in props…
Not sure why you want separate state for the child components. I believe something like this would suffice.
const MasterComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const [isOpen1, setIsOpen1] = useState(false);
const [isOpen2, setIsOpen2] = useState(false);
const handleParentClose = () => {
setIsOpen(false);
setIsOpen1(false);
setIsOpen2(false);
}
return (
<div>
<button onClick={handleParentClose}>toggle isOpen parent</button>
<Accordion isOpen={isOpen1} setIsOpen={setIsOpen1} />
<Accordion isOpen={isOpen2} setIsOpen={setIsOpen2} />
</div>
);
};
const Accordion = props => {
return (
<div>
I'm open: {props.isOpen}
<button onClick={props.setIsOpen}>toggle isOpen child</button>
</div>
);
}
This doesn't include code for actual visibility toggle, but the state is there, including state that closes accordions on parent close.

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