Odd behavior with react-modal - javascript

I'm trying to build a quiz that uses react-modal to provides hints. I will need multiple modals inside the quiz. I'm new to React so it's quite possible that I'm making a simple mistake.
I'm not sure it matters, but I've built this using create-react-app.
My App.js looks like this:
import React, { Component } from 'react';
import HintModal from './hintModal';
import Modal from 'react-modal';
import './App.css';
Modal.setAppElement('#root');
class App extends Component {
state = {
modalIsOpen: false,
hint: ''
};
openModal = (hint) => {
this.setState({ modalIsOpen: true, hint: hint });
}
closeModal = () => {
this.setState({ modalIsOpen: false, hint: '' });
}
render() {
return (
<React.Fragment>
<h1>Modal Test</h1>
<h2>First Modal</h2>
<HintModal
modalIsOpen={this.state.modalIsOpen}
openModal={this.openModal}
closeModal={this.closeModal}
hint="mango"
/>
<hr />
<h2>Second Modal</h2>
<HintModal
modalIsOpen={this.state.modalIsOpen}
openModal={this.openModal}
closeModal={this.closeModal}
hint="banana"
/>
</React.Fragment>
);
}
}
export default App;
hintModal.jsx looks like this:
import React, { Component } from 'react';
import Modal from 'react-modal';
const HintModal = (props) => {
const {openModal, modalIsOpen, closeModal, hint} = props;
return (
<React.Fragment>
<button onClick={ () => openModal(hint) }>Open Modal</button>
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
contentLabel="Example Modal"
>
<h2>Hint</h2>
<p>{hint}</p>
<button onClick={closeModal}>Close</button>
</Modal>
<p>We should see: {hint}</p>
</React.Fragment>
);
};
export default HintModal;
Here's the problem: I need the content of the modal to change based on the hint prop passed to HintModal. When I output hint from outside <Modal>, it behaves as expected, displaying the value of the prop. But when I output hint within <Modal>, it returns "banana" (the value of the hint prop for the second instance of HintModal) when either modal is activated.
Any help would be greatly appreciated!

You are controlling all of your modals with the same piece of state and the same functions to open and close the modal.
You need to either have just one modal and then dynamically render the message inside it or you need to store a modalIsOpen variable in your state for every single modal.

Related

React Custom Modal Component Problems With Updating Same Component Used Repeatedly

I built a custom Modal.
There is one particular function I would like it to do when opened. I would like a CSS class to be toggled when this modal is opened/closed.
This works just fine if I only insert this component once in a template. But in my case I am inserting it three times. By using the componentDidMount I insert some JS that should toggle the CSS class. It does not do it for the first or the second modal, it will only do it for the third.
CODE UPDATED!
This is the parent component:
import React from "react";
import ModalSmall from "./ModalSmall";
import ModalMedium from "./ModalMedium";
import ModalLarge from "./ModalLarge";
import "bootstrap/dist/css/bootstrap.css";
import "./styles.scss";
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
isModalSmallOpen: false,
isModalMediumOpen: false,
isModalLargeOpen: false
};
}
toggleModalSmall = (e) => {
e.preventDefault();
this.setState((prev) => ({
...prev,
isModalSmallOpen: !prev.isModalSmallOpen
}));
};
toggleModalMedium = (e) => {
e.preventDefault();
this.setState((prev) => ({
...prev,
isModalMediumOpen: !prev.isModalMediumOpen
}));
};
toggleModalLarge = (e) => {
e.preventDefault();
this.setState((prev) => ({
...prev,
isModalLargeOpen: !prev.isModalLargeOpen
}));
};
render() {
return (
<div className="container">
<div className="row">
<div className="col">
<h1>Hello Y'all!</h1>
<p className="yo-green">My Modal Samples</p>
<div className="row mt-5">
<div className="col">
<button
className="btn btn-primary"
onClick={this.toggleModalSmall}
>
Modal Small
</button>
</div>
<div className="col">
<button
className="btn btn-primary"
onClick={this.toggleModalMedium}
>
Modal Medium
</button>
</div>
<div className="col">
<button
className="btn btn-primary"
onClick={this.toggleModalLarge}
>
Modal Large
</button>
</div>
</div>
</div>
</div>
<ModalSmall
modalName="smallModal"
modalTitle="Small Modal"
modalBody="This is the small modal!"
toggleModal={this.toggleModalSmall}
modalOpen={this.state.isModalSmallOpen}
/>
<ModalMedium
modalName="mediumModal"
modalTitle="Medium Modal"
modalBody="This is the medium modal!"
toggleModal={this.toggleModalMedium}
modalOpen={this.state.isModalMediumOpen}
/>
<ModalLarge
modalName="largeModal"
modalTitle="Large Modal"
modalBody="This is the LARGE modal!"
toggleModal={this.toggleModalLarge}
modalOpen={this.state.isModalLargeOpen}
/>
</div>
);
}
}
One of the in-between components:
import React from "react";
import Modal from "./Modal";
const ModalSmall = (props) => {
return (
<Modal
modalName={props.modalName}
modalTitle={props.modalTitle}
modalBody={props.modalBody}
toggleModal={props.toggleModal}
modalOpen={props.modalOpen}
/>
);
};
export default ModalSmall;
Here is my modal Component:
import React from "react";
export default class Modal extends React.Component {
componentDidUpdate() {
if (this.props.modalOpen) {
console.log("Open!", this.props.modalOpen);
document.body.classList.add("drawer-open");
} else {
console.log("Closed!", this.props.modalOpen);
document.body.classList.remove("drawer-open");
}
}
render() {
return (
<div className="mymodal" id={this.props.modalName}>
<div
onClick={this.props.toggleModal}
className={`mymodal-overlay ${this.props.modalOpen && "active"}`}
></div>
<div
className={`mymodal-content d-flex flex-column ${
this.props.modalOpen && "active"
}`}
>
<header className="p-2 border-bottom d-flex">
<span
className="material-icons clickable"
onClick={this.props.toggleModal}
>
close
</span>
<div className="flex-grow-1 ml-2">{this.props.modalTitle}</div>
</header>
<div className="p-2 flex-grow-1">{this.props.modalBody}</div>
<footer className="p-2 border-top">© ChidoPrime 2021</footer>
</div>
</div>
);
}
}
Working Sample Here with Solution Applied
UPDATE! -------------
There is a second approach I would like to include, different than the checked answer offered by #sanishJoseph. In which I add a constructor and declare a state within the modal controller. Without the need of using React.PureComponent. I use preProvs within the componentDidUpdate. Code for the modal follows:
constructor(props) {
super(props);
this.state = {
modalOpen: false
};
}
componentDidUpdate(prevProps) {
if (prevProps.modalOpen === this.props.modalOpen) return;
if (this.props.modalOpen) {
console.log("Open!", this.props.modalOpen);
document.body.classList.add("drawer-open");
} else {
console.log("Closed!", this.props.modalOpen);
document.body.classList.remove("drawer-open");
}
}
Second Sample using prevProps without using React.PureComponent
I think the biggest mistake is in your Parent component. Your initial state of the page is
this.state = {
isModalSmallOpen: false,
isModalMediumOpen: false,
isModalLargeOpen: false
}
But, when you open a Modal, you are setting your state to one item in the state, rest of the items are going null. Meaning, when you do
this.setState({
isModalSmallOpen: !this.state.isModalSmallOpen
})
You are setting isModalMediumOpen: null, isModalLargeOpen: null.
What you should be doing is,
this.setState((prev) => ({...prev,
isModalSmallOpen: !prev.isModalSmallOpen
}))
So all of your states will remain in your state. This change is needed in all the 3 modal opening functions.
Update :
Fix is petty easy. All you need to do is add a react.memo if it was a functional component. In your case make your Modal component as a PureComponent.
export default class Modal extends React.PureComponent
Pure Components in React are the components which do not re-renders
when the value of state and props has been updated with the same
values.
https://codesandbox.io/s/my-custom-modal-forked-yg4vo?file=/src/App.js
The code is a little complex to understand, but I think the main problem is with the logic used to implement it. If I understood correctly you are using the same Component more than once. So, each component executes componentDidUpdate method each time that is rerendered.
What this means is that if you are toggling one of your modals in the "parent" component with the methods "toggleModal..." then, the parent render method is executed and it executes each render children method. What happened there is that with your first modal you are adding o removing the body css, with the second you are doing the inverse and with the third, you are adding and removing again.
You have a lot of things to get better there, but the most simple is use the arguments you got in your componentDidUpdated method and make sure you only executed your code if the new props changes. This going to solve your problem.

react-ultimate-pagination component setup

I'm trying to use this package react-ultimate-pagination: https://github.com/ultimate-pagination/react-ultimate-pagination
I want to set it up like their basic demo example: https://codepen.io/dmytroyarmak/pen/GZwKZJ
The usage instructions at the bottom of the github page say to import the component like this:
import ReactUltimatePagination from 'react-ultimate-pagination';
But the codepen demo just shows a constant:
const UltimatePagination = reactUltimatePaginationBasic.default;
I copied the code from the demo, but since it is mismatched with the import, I have an error of UltimatePagination being undefined and reactUltimatePaginationBasic undefined.
Does anyone know how to set up this component like the demo example?
The module exports the higher oder component createUltimatePagination as a named export. To import it using es6 import syntax it has to be the following:
import {createUltimatePagination} from 'react-ultimate-pagination';
Example App:
import React, { Component } from "react";
import { render } from "react-dom";
import { createUltimatePagination } from "react-ultimate-pagination";
const Button = ({ value, isActive, disabled, onClick }) => (
<button
style={isActive ? { fontWeight: "bold" } : null}
onClick={onClick}
disabled={disabled}
>
{value}
</button>
);
const PaginatedPage = createUltimatePagination({
itemTypeToComponent: {
PAGE: Button,
ELLIPSIS: () => <Button value="..." />,
FIRST_PAGE_LINK: () => <Button value="First" />,
PREVIOUS_PAGE_LINK: () => <Button value="Prev" />,
NEXT_PAGE_LINK: () => <Button value="Next" />,
LAST_PAGE_LINK: () => <Button value="Last" />
}
});
class App extends Component {
state = {
page: 1
};
render() {
return (
<PaginatedPage
totalPages={10}
currentPage={this.state.page}
onChange={page => this.setState({ page })}
/>
);
}
}
render(<App />, document.getElementById("root"));
Also see this working example on codesandbox.
To be honest I played around with the api of that library and actually it is unclear to me how this library is intended to be used. A pagination component should receive a list of items and then provide a render prop to render the current page with a slice of these items. It's a pagination that does not paginate. Basically it's only a button bar.
Just use var ReactUltimatePagination = require('react-ultimate-pagination'); after you've installed it with npm install react-ultimate-pagination --save

this.refs is not working on bootstrap modal giving null when trying to use with ReactDOM.findDOMNode

I am having problem when I am trying to access a modal ref when it is open.
The following is my wrapper class.
ModalWrapper
import * as React from 'react';
import MyModal from './MyModal'
import * as ReactDOM from "react-dom";
export interface IModalWrapperProps {
}
export class ModalWrapper extends React.PureComponent<IModalWrapperProps, {}> {
constructor() {
super();
}
/**
* EDITED HERE
*/
handlePrint = (refObj) => {
//Getting the ref of the MyMoal component but when I trying to log the data key it is undefined
console.log(refObj); //successfully logs the ref object of MyModal component.
const refVal = refObj.data;
const node = ReactDOM.findDOMNode(refVal);
console.log(node) // logging null still
}
renderDivTag = () => {
return (
<div>
<h1>Hello Modal</h1>
</div>
)
}
render() {
return (
<MyModal id="test-modal" onPrint={this.handlePrint} showComponent={this.renderDivTag()} />
<button onClick={() => showHelloModal('test-modal')} />
)
}
}
MyModal Component
import * as React from 'react';
import { Modal } from 'react-bootstrap';
import { connect } from 'react-redux';
export interface IMyModalProps {
modalID: string;
id: string;
showComponent: React.ComponentClass;
onPrint: (ref) => void;
}
export class MyModalImpl extends React.PureComponent<IMyModalProps, {}> {
constructor() {
super();
}
/**
* EDITED HERE
*/
refValue;
handlePrint = () => {
return this.props.onPrint(this.refValue);
}
render() {
if (this.props.modalID !== this.props.id) {
return <div></div>;
}
return (
<Modal className="print-preview-outer" show={true} >
<Modal.Header closeButton>
<Modal.Title>Print Preview</Modal.Title>
</Modal.Header>
<Modal.Body className="print-preview">
<div
/**
* EDITED HERE
*/
ref=((value) => this.refValue = value)
style={{ width: '597px', background: 'white', margin: 'auto' }}
id="print-preview"
>
{this.props.showComponent}
</div>
</Modal.Body>
<button onClick={this.handlePrint}>Print</button>
</Modal >
)
}
}
export function mapStateToProps(state) {
return {
modalID: state.get('modalID')
}
};
export const MyModal = connect<{}, {}, IMyModalProps>(mapStateToProps)(MyModalImpl)
So when I am clicking the button the showHelloModal method sets a value on the key modalID and then I compare both the modal id and if both is equal then I show the modal.
Now what I am trying to do is I need the DOMNode of the MyModal component in my ModalWrapper Component to print that Hello Modal Word after the modal is displayed.
How can I get a reference to the DOM Node using the ref. If I am using the document.getElementById('print-preview') I am able to access the DOM Node of that modal but I want to use ref instead.
One more thing when I am loggin the this.refs inside my ModalWrapper Component I am getting an Object in the console as follow
The console.log on
Any Help would be appericited.
useRef hook will not work in modals as the component will mount but the jsx will not render until you make show prop to true.
useRef is asynchronous in nature thats why at the time of declaration it sets current to null but after you assign it to any element ref got value of it.
But in case of modals the case is different. Here the elements are not registered instantly but after modal show prop is set to true
To solve this make the modal's show prop always to true and make whole component to show/hide dynamically.
eg.
const Child=()=>{
const customRef=useRef(null);
return(
<Modal show={true}>
<Modal.Body>
<input ref={customRef} type="text"/>
</Modal.Body>
</Modal>
);
}
const Parent=()=>{
const [show,setShow]=useState=(false)
return(
<>
<button onClick={()=>setShow(!show)}>Show/Hide Popup</button>
{
show?<Child/>:null
}
</>
)
}
this will surely solve your problem with using useRef in Modal Popup

Confirming navigation with with custom dialog using setRouteLeaveHook

I am trying to use a custom dialog to ask a user for confirmation before navigating away with unsaved data.
Following the docs I have:
componentDidMount() {
this.props.router.setRouteLeaveHook(
this.props.route,
this.routerWillLeave
)
}
But instead of
routerWillLeave(nextLocation) {
if (!this.props.pristine) {
return 'You have unsaved information, are you sure you want to leave this page?'
}
I have
routerWillLeave(nextLocation) {
if (!this.props.pristine) {
this.setState({open: true})
this.forceUpdatate() //necessary or else render won't be called to open dialog
}
The dialog component I am using comes from material-ui which just expects an open boolean to control the dialog, it also takes a handleCancel and handleContinue methods, but I am not sure how to hook it up with routerWillLeave.
The handleCancel method is simple as it just closes the dialog:
handleCancel() {
this.setState({open: false})
};
I have wrapped the dialog component in component called Notification
export default class Notification extends React.Component {
render() {
const { open, handleCancel, handleContinue } = this.props
const actions = [
<FlatButton
label="Cancel"
primary={true}
onTouchTap={handleCancel}
/>,
<FlatButton
label="Continue"
primary={true}
onTouchTap={handleContinue}
/>,
];
return (
<div>
<Dialog
actions={actions}
modal={false}
open={open}
>
You have unsaved data. Discard changes?
</Dialog>
</div>
);
}
}
And I can call it from the parent component, I have this in the render method:
<Notification open={open} handleCancel={this.handleCancel} handleContinue={this.handleContinue}/>
Basically my question is how can I wire this up with routerWillLeave instead of showing the native browser alert?
When you call createHistory, one of the options to it is getUserConfirmation, which takes a prompt message and a callback. For DOM histories (browserHistory and hashHistory), getUserConfirmation calls window.confirm, passing it the message. The callback function receives the return value of window.confirm [0].
What you need to do is to provide your own getUserConfirmation method that replicates window.confirm. When it gets called, you should display your modal and trigger the callback depending on which button is clicked.
Notification.js
The <Notification> component should take the prompt message and the callback function to call based on the user's action.
class Notification extends React.Component {
contructor(props) {
this.state = {
open: false
}
}
handleCancel() {
this.props.callback(false)
this.setState({ open: false })
}
handleContinue() {
this.props.callback(true)
this.setState({ open: false })
}
render() {
const { message } = this.props
const { open } = this.state
const actions = [
<FlatButton
label="Cancel"
primary={true}
onTouchTap={this.handleCancel.bind(this)}
/>,
<FlatButton
label="Continue"
primary={true}
onTouchTap={this.handleContinue.bind(this)}
/>,
];
return (
<div>
<Dialog
actions={actions}
modal={true}
open={open}
>
{message}
</Dialog>
</div>
);
}
}
ModalConfirmation.js
The confirmation modal isn't really a part of your UI, which is why I am rendering it in a separate render process than the rest of the app.
import React from 'react'
import ReactDOM from 'react-dom'
import Notification from './components/Notification'
export default function = (holderID) => {
var modalHolder = document.getElementById(holderID)
return function ModalUserConfirmation(message, callback) {
ReactDOM.render((
<Notification open={true} message={message} callback={callback} />
), modalHolder)
}
}
This will obviously force you to create your own history object. You cannot just import browserHistory or hashHistory because those use window.confirm. Luckily it is trivial to create your own history. This is essentially the same code as is used in browserHistory [1], but it passes createBrowserHistory your getUserConfirmation function.
createConfirmationHistory.js
import createBrowserHistory from 'history/lib/createBrowserHistory'
import createRouterHistory from './createRouterHistory'
export default function(getUserConfirmation) {
return createRouterHistory(createBrowserHistory({
getUserConfirmation
})
}
index.js
Finally, you need to put this all together.
import createHistory from './createConfirmationHistory'
import ModalConfirmation from './ModalConfirmation'
const getModalConfirmation = ModalConfirmation('modal-holder')
const history = createHistory(getModalConfirmation)
ReactDOM.render((
<Router history={history}>
// ...
</Router>
), document.getElementById('root')
You would have to refactor this a bit if you wanted to use a history singleton, but otherwise it should work. (I haven't actually tested it, though).
[0] https://github.com/mjackson/history/blob/v2.x/modules/DOMUtils.js#L38-L40
[1] https://github.com/ReactTraining/react-router/blob/master/modules/browserHistory.js
You can try using react-router-navigation-prompt.
This worked for me.
I did this using Prompt from react-router-dom with a custom modal from antd. Material UI should have very similar functionality.
In my index.js I set up my dialog in getUserConfirmation of the Router:
import { MemoryRouter as Router } from 'react-router-dom'
import { Modal } from 'antd'
import App from './App'
const { confirm } = Modal
const confirmNavigation = (message, callback) => {
confirm({
title: message,
onOk() {
callback(true)
},
onCancel() {
callback(false)
}
})
}
ReactDOM.render(
<Router getUserConfirmation={confirmNavigation}>
<App />
</Router>
document.getElementById('root')
)
Then you use in it the component you want to prompt if you try to navigation away, using the when prop to give a condition to pop up the modal.
import { Prompt } from 'react-router-dom'
class MyComponent extends React.Component {
render() {
return (
<div>
<Prompt
when={myDataHasNotSavedYet}
message="This will lose your changes. Are you sure want leave the page?"
/>
<RestOfMyComponent/>
</div>
)
}
}

How can I call a component's method from another component in React?

I have a modal component with two methods that show/hide the modal. How can I call those methods from another component?
This is the code for the Modal:
// Dependencies
//==============================================================================
import React from 'react'
import Modal from 'boron/DropModal'
// Class definition
//==============================================================================
export default class RegistrationModal extends React.Component {
showRegistrationModal() {
this.refs.registrationModal.show()
}
hideRegistrationModal() {
this.refs.registrationModal.hide()
}
render() {
return (
<Modal ref="registrationModal" className="modal">
<h2>Meld je aan</h2>
<button onClick={this.hideRegistrationModal.bind(this)}>Close</button>
</Modal>
)
}
}
You can call a components method from the outside as long as you keep a reference to the component. For example:
let myRegistrationModal = ReactDOM.render(<RegistrationModal />, approot );
// now you can call the method:
myRegistrationModal.showRegistrationModal()
It's a bit cleaner if you pass a reference to the modal to another component, like a button:
let OpenModalButton = props => (
<button onClick={ props.modal.showRegistrationModal }>{ props.children }</button>
);
let myRegistrationModal = ReactDOM.render(<RegistrationModal />, modalContainer );
ReactDOM.render(<OpenModalButton modal={ myRegistrationModal }>Click to open</OpenModalButton>, buttonContainer );
Working example: http://jsfiddle.net/69z2wepo/48169/
You cant call it from another component, because its a method belong to RegistrationModal component, but you can refactor your code so you can call it
export function hideRegistrationModal() {
console.log("ok");
}
export default class RegistrationModal extends React.Component {
render() {
return (
<Modal ref="registrationModal" className="modal">
<h2>Meld je aan</h2>
<button onClick={hideRegistrationModal}>Close</button>
</Modal>
)
}
}
now you can call from anywhere but you need to import it first like this
import { RegistrationModal, hideRegistrationModal } from 'path to modal.js'
// ^-- Component name ^-- Method
What you want to do is create a parent component which will handle the communication between your modals.
A really great example and explanation can be found here: ReactJS Two components communicating
This is a good approach because it keeps your components decoupled.

Categories