How to prevent re-render when using react material ui dialogs - reactjs

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.

Related

How to set focus to button in react and material ui on Dialog properly?

When I used code :
<DialogActions>
<Button onClick={handleClose}>Disagree</Button>
<Button onClick={handleClose} autoFocus>
Agree
</Button>
</DialogActions>
it give me the error:
Line 35:51: The autoFocus prop should not be used, as it can reduce usability and accessibility for users jsx-a11y/no-autofocus
So I used different approach:
import { useState, useRef, useEffect } from 'react';
import Button from '#mui/material/Button';
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 PropTypes from 'prop-types';
export default function DialogYesNo(props) {
const { parentShowAlert, parentOnCloseDialog, dialogTitle, dialogText } = props;
const [open, setOpen] = useState(false);
useEffect(() => {
setOpen(parentShowAlert);
}, [parentShowAlert]);
const handleClose = () => {
setOpen(false);
parentOnCloseDialog();
};
const cancelButtonRef = useRef(null);
useEffect(() => {
if (open && cancelButtonRef.current) {
cancelButtonRef.current.focus();
}
}, [open]);
return (
<div>
<Dialog open={open} onClose={handleClose} aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description">
<DialogTitle id="alert-dialog-title">{dialogTitle}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">{dialogText}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} ref={cancelButtonRef}>
No
</Button>
<Button onClick={handleClose}>Agree</Button>
</DialogActions>
</Dialog>
</div>
);
}
DialogYesNo.propTypes = {
parentShowAlert: PropTypes.bool,
parentOnCloseDialog: PropTypes.func,
dialogTitle: PropTypes.string,
dialogText: PropTypes.string
};
But code cancelButtonRef.current.focus() is never fired, because cancelButtonRef is always null and button is not set as focused.
Do you know what is wrong in my code?

How can a parent run a function inside a child which is not known in advance?

When using React 18, if I have a MUI Dialog with Save button and I insert in a TextItem inside it which has a form. How can I submit the form handler inside the inserted TextItem while clicking the Dialog action button "Save"? Note that the inserted TextItem is not known in advance and will be dynamic. So it could be PhotoItem, TextItem, RandomItem. I truly prefer managing the form state and submission handling in that child component. I have looked into refs before using useImperativeHandle but I am not sure if its the best solution.
TextItem.js
import React from 'react'
function TextItem() {
const submit = () => {
console.log('submit');
}
return (
<div>TextItem</div>
)
}
export default TextItem
ContentDialog.js
import { useEffect, useState } from 'react';
import Button from '#mui/material/Button';
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 TextItem from '../space/Screens/TextItem';
import useMediaQuery from "#mui/material/useMediaQuery";
export default function ContentDialog(props) {
const [toggle, setToggle] = useState(true);
const bigScreen = useMediaQuery((theme) => theme.breakpoints.up("md"));
const handleClose = () => {
setToggle(false);
}
const handleOpen = () => {
setToggle(true);
}
useEffect(() => {
setToggle(props.open);
}, [props.open]);
return (
<div>
<Dialog
fullScreen={!bigScreen}
open={toggle}
onClose={props.handleClose}
aria-labelledby="scroll-dialog-title"
aria-describedby="scroll-dialog-description"
>
<DialogTitle id="scroll-dialog-title">Subscribe</DialogTitle>
<DialogContent dividers={scroll === 'paper'}>
<TextItem/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleClose}>Save</Button>
</DialogActions>
</Dialog>
</div>
);
}

Adding Validation and Function to Input using React and MUI

I am creating a cat APP, that pulls a request from thecatapi, I want my submit button to load my ImageGrid.jsx when they input the correct link for example:
in the dialog box you put:
https://api.thecatapi.com/v1/images/search
then it must have a submit loading function and take you to the images of the cats:
what i have is this:
import React, { useState } from 'react';
import PropTypes from 'prop-types'
import getCat from './ImageGrid'
// Material UI
import DialogActions from '#material-ui/core/DialogActions';
import DialogContent from '#material-ui/core/DialogContent';
import DialogTitle from '#material-ui/core/DialogTitle';
import Input from '#material-ui/core/Input'
import Dialog from '#material-ui/core/Dialog';
import Button from '#material-ui/core/Button';
import Grid from '#material-ui/core/Grid';
const DialogBox = ({ ariaLabel }) => {
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
// const handleGrid = () => {
// React.useState(true)
// }
return (
<div className='modal'>
<Grid container justifyContent='center'>
{/* Button To Open Dialog Box */}
<Button
style={{border: '1px solid #ebc340', color: '#ebc340'}}
variant="outlined"
color="secondary"
onClick={handleClickOpen}>
Welcome to my Case Study, Click to begin
</Button>
</Grid>
{/* Dialog Box Content */}
<Dialog
className='dialog-btn'
open={open}
onClose={handleClose}>
<DialogTitle>
To begin the application, please insert your URL below:
</DialogTitle>
<DialogContent>
<Input
placeholder="Enter Your Link Here"
inputProps={ariaLabel}
fullWidth/>
</DialogContent>
{/* Dialog Box Actions */}
<DialogActions>
<Button
onClick={handleClose}
color="secondary">
Cancel
</Button>
<Button
onClick={ getCat }
color='primary'
autoFocus
type='submit'>
{/* onSubmit={handleGrid} */}
Submit
</Button>
</DialogActions>
</Dialog>
</div>
);
}
Input.inputProps = {
ariaLabel: 'Please insert your URL below',
tertiaryColor: 'tertiary'
}
Input.inputTypes = {
ariaLabel: PropTypes.string.isRequired,
tertiaryColor: PropTypes.string.isRequired,
};
export default DialogBox
and then it needs to load this:
/
/ Fetch data from
import React, { useCallback, useEffect, useState } from "react";
const url = "https://api.thecatapi.com/v1/images/search?limit=9";
function ImageGrid() {
const [catUrls, setCatUrls] = useState();
const getCat = useCallback(() => {
console.log("Hello World");
let catImageUrlList = [];
// fetch http request
fetch(url)
.then((res) => res.json()) //gives data in json
.then((cats) => {
console.log("Cats: ", cats);
for (let i = 0; i < cats.length; i++) {
catImageUrlList.push(cats[i].url);
}
setCatUrls(catImageUrlList);
})
.catch((error) => {
console.log("Error: ", error);
});
}, [setCatUrls]);
useEffect(() => {
console.log("Loading your feline friends....");
getCat();
}, [getCat]);
return (
<>
<h1>Look At These Beautiful Kitty's!</h1>
{catUrls ? (
catUrls.map((catUrl) => <img src={catUrl} alt={"kitty"} />)
) : (
<p>Loading...</p>
)}
<button onClick={getCat}>Refresh</button>
</>
);
}
export default ImageGrid;

How to test Material-UI Popover close implementation

I would like to make sure that my implementation of a Popover element combined with a trigger button works as expected.
I was unable to have a working test for asserting that the Popover gets closed after the user presses esc. I was able to make this test work with Modal, but I must use a Popover in my current project.
Component code:
import {
Button, Popover,
} from '#mui/material';
import React from 'react';
export default function SimpleModal() {
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
const id = open ? 'user-menu' : undefined;
return (
<>
<Button
aria-controls={id}
aria-haspopup="true"
onClick={handleClick}
>
Open Modal
</Button>
<Popover open={open} onClose={handleClose}>
<h1>Text in Modal</h1>
</Popover>
</>
);
}
Test file code:
import { render, screen } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
import SimpleModal from './SimpleModal';
test('should close when esc key is pressed', async () => {
render(<SimpleModal />);
userEvent.click(screen.getByText('Open Modal'));
expect(screen.getByText('Text in Modal')).toBeInTheDocument();
userEvent.keyboard('{esc}');
await expect(screen.queryByText('Text in Modal')).not.toBeInTheDocument();
});
As suggested by #juliomalves, wrapping the last expect in a waitFor corrected the test:
await waitFor(() => expect(...));

ReactJS Modal with Material UI

I'm trying to make a reusable confirmation modal with Material UI but when I press CANCEL or OK the modal does not close.
The code is here:
https://codesandbox.io/s/lucid-hoover-sput6?file=/src/App.js
I can't figure it out why the pop don't dissapear.
LE: added the code here so it remains
ConfirmModal.js
import React from "react";
import { Button } from "#material-ui/core";
import Dialog from "#material-ui/core/Dialog";
import DialogActions from "#material-ui/core/DialogActions";
import DialogContent from "#material-ui/core/DialogContent";
import DialogContentText from "#material-ui/core/DialogContentText";
const ConfirmModal = (props) => {
const { content, open, setOpen, onConfirm } = props;
return (
<Dialog
open={open}
onClose={() => setOpen(false)}
aria-labelledby="dialog-title"
>
<DialogContent>
<DialogContentText>{content}</DialogContentText>
</DialogContent>
<DialogActions>
<Button autoFocus onClick={() => setOpen(false)} color="primary">
Cancel
</Button>
<Button
onClick={() => {
setOpen(false);
onConfirm();
}}
color="primary"
>
OK
</Button>
</DialogActions>
</Dialog>
// </div>
);
};
export default ConfirmModal;
App.js
import React, { useState } from "react";
import { IconButton } from "#material-ui/core";
import { Input as InputIcon } from "#material-ui/icons";
import ConfirmModal from "./ConfirmModal";
export default function App() {
const [confirmOpen, setConfirmOpen] = useState(false);
const handleLogout = () => {
console.log("this hould logout the user");
};
return (
<div className="App">
<h2>Press the button below so the confirmation modal appears </h2>
<IconButton color="inherit" onClick={() => setConfirmOpen(true)}>
<InputIcon />
<ConfirmModal
content="Are you sure you want to leeeave us ?"
open={confirmOpen}
setOpen={setConfirmOpen}
onConfirm={handleLogout}
/>
</IconButton>
</div>
);
}
Move the modal out of the button. The Modal's cancel/confirm/backdrop click events are propagating (bubbling) up to the open button (IconButton) and its onClick handler is just reopening the modal by setting confirmOpen state true.
export default function App() {
const [confirmOpen, setConfirmOpen] = useState(false);
const handleLogout = () => {
console.log("this hould logout the user");
};
return (
<div className="App">
<h2>Press the button below so the confirmation modal appears </h2>
<IconButton color="inherit" onClick={() => setConfirmOpen(true)}>
<InputIcon />
</IconButton>
<ConfirmModal
content="Are you sure you want to leeeave us ?"
open={confirmOpen}
setOpen={setConfirmOpen}
onConfirm={handleLogout}
/>
</div>
);
}

Resources