I'm trying to turn Material UI's dialog into a "useDialog" hook so it keeps track of it's own open state.
Unfortunately I've encountered a problem that whenever I update a state further up the hierarchy, the dialog flickers and I'm not exactly sure why and how to circumvent it. I feel like a useRef is needed there somewhere, but I'm not sure. Here's a reproduced minimal example: https://codesandbox.io/s/flickering-dialog-minimal-example-ehruf?file=/src/App.js
And the code in question:
import React, { useState } from "react";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle
} from "#material-ui/core";
export default function App() {
const [openDialog, Dialog] = useDialog();
const [counter, setCounter] = useState(0);
return (
<div>
<Dialog title="Hello">
<div>{counter}</div>
<button onClick={() => setCounter(counter => counter + 1)}>
Increase
</button>
</Dialog>
<button onClick={openDialog}>Open dialog</button>
</div>
);
}
const useDialog = () => {
const [open, setOpen] = useState(false);
const handleClose = () => {
setOpen(false);
};
const someDialog = ({ title, children }) => {
return (
<Dialog open={open} onClose={handleClose}>
<DialogTitle>{title}</DialogTitle>
<DialogContent>{children}</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Close
</Button>
</DialogActions>
</Dialog>
);
};
return [
() => {
setOpen(true);
},
someDialog
];
};
The reason the dialog flickers is that a new Dialog component is created on every render(as a result of state change) in App. The old Dialog is unmounted and replaced by the new Dialog.
A rule of thumb is you should never define components while rendering.
That's why I suggest you separate your custom dialog component from useDialog hook:
const MyDialog = ({ open, handleClose, title, children }) => {
return (
<Dialog open={open} onClose={handleClose}>
<DialogTitle>{title}</DialogTitle>
<DialogContent>{children}</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Close
</Button>
</DialogActions>
</Dialog>
);
};
You can, however, keep some of the logic inside useDialog and reuse them:
const useDialog = () => {
const [open, setOpen] = useState(false);
const openDialog = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const props = {
open,
handleClose
};
return [openDialog, props];
};
More about why returning components from hook can be a bad idea.
Custom hooks are not made for returning a component, instead they are used to create a common logic which will be shared by different components.
In your case I would suggest you to create a common component for your dialog. And use this component wherever you want. Like this:
<CustomDialog open={open}>
// Your jsx here
</CustomDialog>
const CustomDialog = ({children}) => {
return <Dialog open={open} onClose={handleClose}>
<DialogTitle>{title}</DialogTitle>
<DialogContent>{children}</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Close
</Button>
</DialogActions>
</Dialog>
}
For more information about custom hooks:
https://reactjs.org/docs/hooks-custom.html
To whomever finds this issue because of my mistake. I was using a styled Dialog and defined the styled Dialog inside the functional component. Just put the custom styling outside of any component.
// This needed to be outstide of the BetterDialog component
const CustomDialog = styled(Dialog)({
"& .MuiDialog-paper": {
zIndex: 90
}
})
...
function BetterDialog(props: BetterDialogProps) {
...
Related
I have a React component that gets data from a parent page/component. I use this data object (jobData) to populate a hook variable value when the component (a modal) is fired. The jobData looks like this: [{id: '17003', file_name: 'My_File', type: 'Medium', state: 'Arkansas'}]. In the browser debugger, I can see the jobData getting passed into the component, but when it gets to return ....<TextField .... value={productID} the productID says undefined! Any suggestions as to what I am doing wrong? I want the TextField to display the value of jobData[0]['id'] when it fires and then store the value of productID when it cahnges.
import React, { useState, useEffect } from 'react';
import { Button, TextField, Dialog, DialogActions, DialogContent, DialogTitle, Modal,
FormControl, Select, InputLabel } from '#mui/material';
import { createTheme, ThemeProvider } from '#mui/material/styles';
export default function ScheduleProdsModal({jobData}) {
const [open, setOpen] = useState(false);
const handleClose = () => setOpen(false);
const handleOpen = () => setOpen(true);
// Below is the hook with problems
let [productID, setProductID] = useState(jobData[0]["id"]);
return (
<div>
<ThemeProvider theme={theme}>
<Button color="neutral" variant="contained" cursor="pointer" onClick={handleOpen}>Schedule Products</Button>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>Schedule Products</DialogTitle>
<DialogContent >
<FormControl fullWidth style={{marginTop: '5px', marginBottom: '5px'}}>
<TextField
autoFocus
margin="dense"
width="100%"
id="my_id"
label="My ID"
type="text"
value={productID}
variant="outlined"
onChange={(e) => {
setProductID(e.target.value);
}}
/>
</FormControl>
</DialogContent>
<DialogActions style={{marginRight: 'inherit'}}>
<Button color="neutral" variant="contained" cursor="pointer" onClick={handleClose}>Close</Button>
<Button color="neutral" variant="contained" cursor="pointer" onClick={handleScheduler}>Schedule</Button>
</DialogActions>
</Dialog>
</ThemeProvider>
</div>
);
}
It seems like you have set jobData array when initialising the projectId, which will be undefined in the first load unless it has been handled in the parent component. Therefore as a workaround we normally use useEffect hook to get the upcoming props changes. So, when the jobData array is not null, you can use setProductID to get the relevant product id inside the useEffect hook. For the time period that you are not getting the data, you can use your favourite loading mechanism to show a loading screen. Find the below code snippet.
const [open, setOpen] = useState(false);
const handleClose = () => setOpen(false);
const handleOpen = () => setOpen(true);
// Below is the hook with problems
let [productID, setProductID] = useState(0);
useEffect(() => {
if(jobData && jobData.length > 0){
setProductID(Number(jobData[0]["id"]))
}
}, [jobData])
// You can put more conditions like undefined and null checking if you want
if(productID === 0){
// Use your fav loading page or loader here
return <>Loading...</>
}
return (
<div>
<ThemeProvider theme={theme}>
Hope this would help.
I am new here and I saw some related answer but it will use another package and class components, i have functional component and use MUI Dialog for popup but the functionality is only for one modal but I have many modal my code
import React from 'react'
import Image from '../../assets/images/banner1.png'
import Dialog from '#mui/material/Dialog';
import DialogActions from '#mui/material/DialogActions';
import DialogContent from '#mui/material/DialogContent';
import DialogContentText from '#mui/material/DialogContentText';
import DialogTitle from '#mui/material/DialogTitle';
import Button from '#mui/material/Button';
export default function Popups() {
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<div className='image-data-grids'>
<div className='image-and-data-section'>
<img onClick={handleClickOpen} className='image-data-grid-image' src={Image} alt='Img' />
</div>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Content1
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Disagree</Button>
</DialogActions>
</Dialog>
<div className='image-and-data-section'>
<img onClick={handleClickOpen} className='image-data-grid-image' src={Image} alt='Img' />
</div>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogContent>
<DialogContentText id="alert-dialog-description">
content2
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Disagree</Button>
</DialogActions>
</Dialog>
</div>
)
}
here handleClickOpen and handleClose are the function for open and close for single modal, i have multiple modals. how customize these two function for multiple modal, i am new in react please help me to solve this issue, Thanks in advance
I know you are talking about having multiple state dependencies within a Component. I think you can do this
function StackOverflow() {
// We create a state dep that holds all our modals open/close state
const [myModals, setMyModals] = useState({
modalA: true,
modalB: false,
})
// This will return an api in which we can toggle, close or open a modal
// ie: set the boolean to true or false
const getModalHanlder = (modalName) => {
return {
isOpen: myModals[modalName],
open: () => setMyModals((state) => ({ ...state, [modalName]: true })),
close: () => setMyModals((state) => ({ ...state, [modalName]: false })),
toggle: () =>
setMyModals((state) => ({ ...state, modalA: !state[modalName] })),
}
}
const modalA = getModalHanlder("modalA")
// Here we invoke our function and pass on the desired modal prop name from
// which we desire to create an api
// We can then create this for another modal, modalB
const modalB = getModalHanlder("modalB")
return (
<div>
<b>MODAL_A: {`${modalA.isOpen}`}</b>
<br />
<button onClick={modalA.toggle}>TOGGLE</button>
<button onClick={modalA.close}>CLOSE</button>
<button onClick={modalA.open}>OPEN</button>
</div>
)
}
This is one approach another one can be to create an state for each modal, like
const [isOpenModalA, setIsOpenModalA] = useState(false)
// And this for each modal state
A fancy one can be to use a hook as useReducer and update each modal state that depends on actions you dispatch, https://reactjs.org/docs/hooks-reference.html#usereducer
I'm using modals from the react-material-ui library for a project, and I noticed a side effect when trying to open/close the dialog component. Try this code (code sandbox):
import { useState } from "react";
import moment from "moment";
import Button from "#material-ui/core/Button";
import Dialog from "#material-ui/core/Dialog";
import DialogTitle from "#material-ui/core/DialogTitle";
import DialogActions from "#material-ui/core/DialogActions";
import "./styles.css";
export default function App() {
const [open, setOpen] = useState(false);
const timeStamp = moment().format("HH:mm:ss SS");
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<div className="App">
<h1>Mui Dialog</h1>
<h2>Rendered on {timeStamp}</h2>
<Button variant="outlined" color="primary" onClick={handleOpen}>
Open simple dialog
</Button>
<Dialog open={open}>
<DialogTitle>This is a simple dialog</DialogTitle>
<DialogActions>
<Button onClick={handleClose} color="primary" autoFocus>
Close
</Button>
</DialogActions>
</Dialog>
</div>
);
}
You'll see that clicking on the button open will cause a re-render and closing the dialog will have the same effect, it's normal because state changed after calling hooks. This is often undesirable, I don't want to re-render the page when opening or after closing.
So I tried to use the following solution, based of refs (code sandbox):
import { useState, useRef, forwardRef, useImperativeHandle } from "react";
import moment from "moment";
import Button from "#material-ui/core/Button";
import Dialog from "#material-ui/core/Dialog";
import DialogTitle from "#material-ui/core/DialogTitle";
import DialogActions from "#material-ui/core/DialogActions";
import "./styles.css";
const SimpleDialog = forwardRef(({ title }, ref) => {
const [open, setOpen] = useState(false);
const innerRef = useRef();
const handleClose = () => {
setOpen(false);
};
useImperativeHandle(ref, () => ({
openDialog: () => setOpen(true),
closeDialog: () => setOpen(false)
}));
return (
<Dialog open={open} ref={innerRef}>
<DialogTitle>{title}</DialogTitle>
<DialogActions>
<Button onClick={handleClose} color="primary" autoFocus>
Close
</Button>
</DialogActions>
</Dialog>
);
});
export default function App() {
const timeStamp = moment().format("HH:mm:ss SS");
const dialogRef = useRef();
const handleOpen = () => {
dialogRef.current.openDialog();
};
return (
<div className="App">
<h1>Mui Dialog</h1>
<h2>Rendered on {timeStamp}</h2>
<Button variant="outlined" color="primary" onClick={handleOpen}>
Open simple dialog
</Button>
<SimpleDialog ref={dialogRef} title="This is a simple dialog" />
</div>
);
}
My question is: is this a correct approach to solve the problem of unwanted re-renders in the case of react-material-ui modals ?
Regards.
My goal is to control child component by Material UI Icon click in React with Typescript app.
So I wish to know that how to access to a child component with useState() or useRef() from a parent one. thank you in advanced.
//Parent.tsx as a parent component
...
import ChildComponent from '/FormDialog';
export default function ParentComponent() {
const classes = useStyles();
return (
...
<ChildCareIcon /> // a Material UI Icon
...
)
//Child.tsx as a child component
...
export default function ChildComponent() {
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<div>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
Open form dialog
</Button>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="form-dialog-title"
>
...
);
use the [open,setOpen] in parent and pass as props to the child
//Parent.tsx as a parent component
...
import ChildComponent from '/FormDialog';
export default function ParentComponent() {
const [open, setOpen] = React.useState(false);
const classes = useStyles();
return (
...
<ChildComponent open={open} setOpen={setOpen} />
<ChildCareIcon /> // a Material UI Icon
...
)
//Child.tsx as a child component
...
export default function ChildComponent({open,setOpen}) {
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<div>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
Open form dialog
</Button>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="form-dialog-title"
>
...
);
or else use them in context
I couldn't declare the same-named property already defined in the API and inject it. So I solved it by replacing the trigger with the icon that I want to make as below.
//Parent.tsx as a parent component
...
import ChildComponent from '/FormDialog';
export default function ParentComponent() {
const classes = useStyles();
return (
...
// <ChildCareIcon /> // move to child component as trigger button.
<ChildComponent />
...
)
//Child.tsx as a child component
import ChildCareIcon from '#material-ui/icons/ChildCare';
...
export default function ChildComponent() {
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<div>
//<Button variant="outlined" color="primary" onClick={handleClickOpen}>
// Open form dialog
//</Button>
// injected from parent component with onClick method.
<ChildCareIcon onClick={handleClickOpen} fontSize="inherit" />
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="form-dialog-title"
>
...
);
I am use bootstrap modal in reactjs project. Here is the link of package which i have installed in my project: https://www.npmjs.com/package/react-responsive-modal
When, I click on open the modal button then it is working, but when i click on close button then close button is not working. I am using the hooks in my project. Below, I have mentioned my code:
import React, { useState } from 'react'
import Modal from 'react-responsive-modal'
const Login = () => {
const [open, openModal] = useState(false)
const onOpenModal = () => {
openModal({open: true})
};
const onCloseModal = () => {
openModal({open: false})
};
return(
<div>
<h1>Login Form</h1>
<button onClick={onOpenModal}>Open modal</button>
<Modal open={open} onClose={onCloseModal} center>
<h2>Simple centered modal</h2>
</Modal>
</div>
)
}
export default Login;
The issue is because, you are setting object in state,
openModal({open: true})
This will store object in state.
setState require's direct value which needs to be change, your setState must be this,
const onOpenModal = () => {
openModal(!open) //This will negate the previous state
};
const onCloseModal = () => {
openModal(!open) //This will negate the previous state
};
Demo
You can simplify your code and just use 1 change handle for your modal,
const Login = () => {
const [open, openModal] = useState(false)
const toggleModal = () => {
openModal(!open)
};
return(
<div>
<h1>Login Form</h1>
<button onClick={toggleModal}>Open modal</button>
<Modal open={open} onClose={toggleModal} center>
<h2>Simple centered modal</h2>
</Modal>
</div>
)
}
Demo
Your naming of the model hook is misleading and you're using the setState part of the Hook wrong, probably mixing it up with the this.setState convention for non-Hook React code.
import React, { useState } from 'react'
import Modal from 'react-responsive-modal'
const Login = () => {
const [modalOpen, setModalOpen] = useState(false)
const onOpenModal = () => {
setModalOpen(true)
};
const onCloseModal = () => {
setModalOpen(false)
};
return(
<div>
<h1>Login Form</h1>
<button onClick={onOpenModal}>Open modal</button>
<Modal open={modalOpen} onClose={onCloseModal} center>
<h2>Simple centered modal</h2>
</Modal>
</div>
)
}
export default Login;