I have the following problem my selected option dissapears after i close and reopen modal , someone told me this is because all data gets lost when a modal unmounts? So how do i cause the selected data to still show.
I am able to retrieve the selected data but all i want is the selected option to still be there once modal closes and reopens
Also My select component is not reusable any clue how i could optimize that piece of code
import React from 'react';
import { makeStyles } from '#material-ui/core/styles';
import Modal from '#material-ui/core/Modal';
import Backdrop from '#material-ui/core/Backdrop';
import Fade from '#material-ui/core/Fade';
import Button from '../Button/Button'
const useStyles = makeStyles(theme => ({
modal: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
paper: {
backgroundColor: theme.palette.background.paper,
border: '2px solid #000',
boxShadow: theme.shadows[5],
padding: theme.spacing(2, 4, 3),
},
}));
const TransitionsModal =(props) => {
const classes = useStyles();
const [open, setOpen] = React.useState(false);
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<div>
<Button clicked={handleOpen}></Button>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
className={classes.modal}
open={open}
onClose={handleClose}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
>
<Fade in={open}>
{props.children}
</Fade>
</Modal>
</div>
);
}
export default TransitionsModal
Here is my select component
import React from 'react';
import InputLabel from '#material-ui/core/InputLabel';
import FormControl from '#material-ui/core/FormControl';
import Select from '#material-ui/core/Select';
export default function NativeSelects(props) {
const [value, setValue] = React.useState('');
const handleChange = event => {
setValue(event.target.value);
console.log(event.target.value)
};
const data = props.list.map((item, id) => {
if (item.reciter_name_eng) {
return <option key={item.reciter_name_eng + id} value={item.id}>{item.reciter_name_eng} {item.style}</option>
}
//if its not null
if (item.name_simple) {
return <option key={item.name_simple + id} value={item.chapter_number}>{item.chapter_number}. {item.name_simple}</option>
}
if (item.language_name) {
return <option key={item.language_name + id} value={item.id}>{item.language_name} by {item.author_name}</option>
}
return null
})
return (
<div>
<FormControl variant="filled">
<InputLabel htmlFor="filled-age-native-simple">{props.type}</InputLabel>
<Select
native
value={value}
onChange={(event) => { props.changed(event.target.value, props.type); handleChange(event) }}>
<option value={null}> </option>
}
{data}
</Select>
</FormControl>
</div>
);
}
The problem is the default state of your Function Component is always an empty string i.e
const [value, setValue] = useState('');
When the modal is closed, the components are all unmounted, meaning when the modal is then reshown these are remounted and as a result the value will go back to an empty string.
You need to keep track of the selected value TransitionModel component and then pass this back into NativeSelects as a prop in order to set the default value i.e.
<NativeSelect defaultValue={...} />
Then in your component
function NativeSelect(props) {
const [value, setValue] = useState(props.defaultValue);
...
}
Related
My form was working perfectly earlier and now on after few changes the state change the input filed is loosing focus. I am using MUI and it was working earlier perfectly but suddenly it started losing focus after few minor change in state variable names. I think that is not the issue but i don't know why is this happening as all controlled forms are made like this i presume
import { useEffect, useState } from "react";
import Axios from "axios";
import { Schedule } from "./Schedule";
import { PageHeader } from "./PageHeader";
import { Button, TextField, styled, CircularProgress } from "#mui/material";
import FileUploadIcon from "#mui/icons-material/FileUpload";
import { DesktopDatePicker } from "#mui/x-date-pickers/DesktopDatePicker";
import { LocalizationProvider } from "#mui/x-date-pickers/LocalizationProvider/LocalizationProvider";
import { AdapterDayjs } from "#mui/x-date-pickers/AdapterDayjs";
import SendIcon from "#mui/icons-material/Send";
function Form() {
const [formData, setFormData] = useState({ startTime: "", endTime: "" });
const [file, setFile] = useState("");
const [data, setData] = useState("");
const [newValue, setValue] = useState(null);
const [loading, setLoading] = useState(false);
console.log(formData);
console.log("Rerendered");
function handleChange(event) {
setFormData((previousFormData) => {
return {
...previousFormData,
[event.target.name]: event.target.value,
};
});
}
function handleClick(event) {
setLoading(true);
event.preventDefault();
setTimeout(() => {
const postFormData = new FormData();
postFormData.append("startTime", formData.startTime);
postFormData.append("endTime", formData.endTime);
postFormData.append("date", newValue);
postFormData.append("file", file);
Axios.post("http://localhost:3001/aip", postFormData, {
headers: {
"Access-Control-Allow-Origin": "cors",
},
})
.then((res) => setData(res.data))
.then(setLoading(false))
.catch((err) => console.log(err));
}, 2000);
}
const Container = styled("main")({
marginTop: 50,
display: "grid",
justifyItems: "center",
});
const Div = styled("div")({
display: "flex",
gap: 50,
});
const Form = styled("form")({
border: "none",
width: "100%",
height: "100%",
margin: "auto",
display: "grid",
justifyItems: "center",
gap: 50,
});
return (
<>
<PageHeader />
<Container>
<Form onSubmit={handleClick} encType="multipart/form-data">
<Div class="timeInput">
<TextField
label="From"
name="startTime"
variant="outlined"
placeholder="9:00"
onChange={handleChange}
value={formData.startTime}
required
/>
<TextField
label="To"
name="endTime"
variant="outlined"
placeholder="9:30"
onChange={handleChange}
value={formData.endTime}
required
/>
</Div>
<div class="dateInput">
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DesktopDatePicker
value={newValue}
label="Choose Date"
onChange={(newValue) => setValue(newValue)}
renderInput={(params) => <TextField {...params} />}
/>
</LocalizationProvider>
</div>
<Div>
<Button sx={{ gap: 2 }} variant="contained" component="label">
Upload Schedule
<input
hidden
accept=".xlsx"
multiple
type="file"
onChange={(event) => {
const file = event.target.files[0];
setFile(file);
}}
/>
<FileUploadIcon />
</Button>
<Button sx={{ gap: 2 }} variant="contained" component="label">
Search
<input hidden type="submit" />
<SendIcon />
</Button>
</Div>
</Form>
</Container>
{loading ? (
<CircularProgress sx={{ alignItems: "center" }} />
) : (
<Schedule data={data} />
)}
</>
);
}
export default Form;
In this example, whenever the react state changes, App will re-render. And because App re-renders, Form gets re-declared and the internal state of the underlying DOM node (form) is lost.
export default function App() {
const [value, setValue] = useState("");
const Form = styled("form")({});
return (
<Form>
<input value={value} onChange={({ target }) => setValue(target.value)} />
</Form>
);
}
Declare Form outside of the body of App and it works
export default function App() {
const [value, setValue] = useState("");
return (
<Form>
<input value={value} onChange={({ target }) => setValue(target.value)} />
</Form>
);
}
const Form = styled("form")({});
If you must declare Form in the body of App (you probably don't need to), you can memoise it with React.useMemo
export default function App() {
const [value, setValue] = useState("");
const Form = useMemo(() => styled("form")({}), []);
return (
<Form>
<input value={value} onChange={({ target }) => setValue(target.value)} />
</Form>
);
}
You should move your styled components outside of the main Form component as they are causing re renders. Just note that you will then have clashing names. Styled Form and the actual component Form. So you will have to either rename the component or the styled Form. As to why this happens, I'm not entirely sure but ran into a similar issue before. I assume that every time you type and the state changes, it's re creating the styled components and then re rendering the main component.
// Imports
import { useEffect, useState } from "react";
import Axios from "axios";
import { Schedule } from "./Schedule";
import { PageHeader } from "./PageHeader";
import { Button, TextField, styled, CircularProgress } from "#mui/material";
import FileUploadIcon from "#mui/icons-material/FileUpload";
import { DesktopDatePicker } from "#mui/x-date-pickers/DesktopDatePicker";
import { LocalizationProvider } from "#mui/x-date-pickers/LocalizationProvi/LocalizationProvider";
import { AdapterDayjs } from "#mui/x-date-pickers/AdapterDayjs";
import SendIcon from "#mui/icons-material/Send";
// Styles
const Container = styled("main")({
marginTop: 50,
display: "grid",
justifyItems: "center",
});
const Div = styled("div")({
display: "flex",
gap: 50,
});
// Component
const Form = styled("form")({
border: "none",
width: "100%",
height: "100%",
margin: "auto",
display: "grid",
justifyItems: "center",
gap: 50,
});
function Form() {
// ...
}
export default Form;
I am using Material UI (V4). There is a Customized SnackBar component being used to display the messages from backed, like success and failure messages. But, I am unable to copy the message shown on the snackbar component. This issue is seen only on Chrome (the text selection is working fine on Safari browser). Here is the sample code I'm using:
import React from 'react';
import Button from '#material-ui/core/Button';
import Snackbar from '#material-ui/core/Snackbar';
import MuiAlert from '#material-ui/lab/Alert';
import { makeStyles } from '#material-ui/core/styles';
import { Icon } from '#material-ui/core';
import SmsFailedIcon from '#material-ui/icons/SmsFailed';
const successIcon = () => (
<Icon>
<SmsFailedIcon />
</Icon>
);
const failedIcon = () => (
<Icon>
<SmsFailedIcon />
</Icon>
);
function Alert(props) {
return (
<MuiAlert
elevation={6}
variant="filled"
{...props}
iconMapping={{ success: successIcon, error: failedIcon }}
/>
);
}
const useStyles = makeStyles((theme) => ({
root: {
width: '100%',
'& > * + *': {
marginTop: theme.spacing(2),
},
},
}));
export default function CustomizedSnackbars() {
const classes = useStyles();
const [open, setOpen] = React.useState(false);
const handleClick = () => {
setOpen(true);
};
const handleClose = (event, reason) => {
setOpen(false);
};
return (
<div className={classes.root}>
<Button variant="outlined" onClick={handleClick}>
Open success snackbar
</Button>
<Snackbar open={open} autoHideDuration={6000} onClose={handleClose}>
<Alert onClose={handleClose} severity="success">
This is a success message!
</Alert>
</Snackbar>
</div>
);
}
Can anyone please advise ?
I added a TextField from the MUI library, and used a useRef hook to capture the value "live" as the user types something. The intention is to filter only the rates which include the characters he types. As of right now:
Object.keys(rates["rates"]) // ["EUR", "RON", "CZK", ...]
I added a form, and I want it to stay persistent, but the buttons should change dynamically. If the user has not typed anything I want to return everything (like nothing is filtered)
My try:
import React, {useEffect, useRef, useState} from 'react'
import Button from '#mui/material/Button';
import CircularProgress from '#mui/material/CircularProgress';
import Box from '#mui/material/Box';
import TextField from '#mui/material/TextField';
const RatesButton = () => {
const rateRef = useRef('')
const [rates, setRates] = useState([]);
useEffect(
() => {
fetch("https://api.vatcomply.com/rates")
.then(ratesResponse => ratesResponse.json())
.then(rates => setRates(rates));
}
, [])
if (rates.length === 0) {
return (
<Box sx={{display: 'flex', justifyContent: 'center'}}>
<CircularProgress/>
</Box>
)
}
const rateSearch = () => {
Object.keys(rates["rates"]).filter(
rate => rate.includes(rateRef.current.value)
).map(rate => {
return (
<Button>
{rate}
</Button>
)
}
)
}
return (
<>
<br/>
<TextField id="rate-search" onChange={rateSearch} inputRef={rateRef} label="Rate" variant="outlined"/>
</>
)
}
export default RatesButton
It works nicely I think, I can access the reference of the input of the user, filter all the rates that contain the letters, and map each one to a MUI Button. The problem is that they don't show somehow, and I am pretty lost, it is pretty confusing how I can return from two different functions at the same time, while keeping one persistent (the input field)
The buttons do not show unfortunately...
You should use controlled mode and store your TextField's value in a state using useState instead of useRef because changing the ref value doesn't trigger a re-render so the UI doesn't get updated. There are a lot of other wrong things in your code, I've fixed it all, feel free to ask me if you don't understand anything:
const RatesButton = () => {
const [value, setValue] = useState("");
const [rates, setRates] = useState({});
useEffect(() => {
fetch("https://api.vatcomply.com/rates")
.then((ratesResponse) => ratesResponse.json())
.then((rates) => setRates(rates.rates ?? {}));
}, []);
return (
<>
{Object.keys(rates).length === 0 && (
<Box sx={{ display: "flex", justifyContent: "center" }}>
<CircularProgress />
</Box>
)}
{Object.keys(rates)
.filter((rate) => rate.toLowerCase().includes(value.toLowerCase()))
.map((rate) => {
return <Button>{rate}</Button>;
})}
<br />
<TextField
id="rate-search"
onChange={(e) => setValue(e.target.value)}
// inputRef={value}
label="Rate"
variant="outlined"
/>
</>
);
};
Live Demo
import React, {useState} from 'react';
import { throttle } from 'lodash';
const RatesButton = () => {
const [value, setValue] = useState("");
const [rates, setRates] = useState({});
useEffect(() => {
fetch("https://api.vatcomply.com/rates")
.then((ratesResponse) => ratesResponse.json())
.then((rates) => setRates(rates.rates ?? {}));
}, []);
const handleChange = (e) => setValue(e.target.value);
// it will prevent multiple render during fast typing
const throttledChange = throttle(handleChange, 400);
return (
<>
{Object.keys(rates).length === 0 && (
<Box sx={{ display: "flex", justifyContent: "center" }}>
<CircularProgress />
</Box>
)}
{Object.keys(rates)
.filter((rate) => rate.toLowerCase().includes(value.toLowerCase()))
.map((rate) => {
return <Button>{rate}</Button>;
})}
<br />
<TextField
id="rate-search"
onChange={throttledChange} // don't use arrow function
label="Rate"
variant="outlined"
/>
</>
);
};
I'm not 100% sure what's going on here. I've got a display component that displays a bunch of cards, using a map based on my database - On the card is an edit button that pops a modal up, passing props over to the edit form.. Here's kinda how it looks:
import React, { useState } from 'react'
import { useQuery, useMutation } from '#apollo/client'
import { GET_ALL_PROJECTS, REMOVE_PROJECT } from '../helpers/queries'
import { makeStyles } from '#material-ui/core/styles'
import DeleteIcon from '#material-ui/icons/Delete'
import EditIcon from '#material-ui/icons/Edit'
import AddForm from './AddForm'
import EditForm from './EditForm'
import AlertMessage from '../Alerts/AlertMessage'
import { Grid, Typography, Card, CardActionArea, CardActions, CardContent, CardMedia, Button, Modal, Backdrop, Fade } from '#material-ui/core'
const useStyles = makeStyles((theme) => ({
modal: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
paper: {
backgroundColor: theme.palette.background.paper,
border: '2px solid #000',
boxShadow: theme.shadows[5],
padding: theme.spacing(2, 4, 3),
},
}));
const DisplayProjects = () => {
const styles = useStyles()
const [deleteItem] = useMutation(REMOVE_PROJECT)
const { loading, error, data } = useQuery(GET_ALL_PROJECTS)
const [status, setStatusBase] = useState('')
const [resultMessage, setResultMessage] = useState('')
const [addOpen, setAddOpen] = useState(false)
const [editOpen, setEditOpen] = useState(false)
const onDelete = (id, e) => {
e.preventDefault()
deleteItem({
variables: { id },
refetchQueries: [{ query: GET_ALL_PROJECTS }]
}).then(
res => handleSuccess(res),
err => handleError(err)
)
}
// Handles Result of the Delete Operation
const handleSuccess = (res) => {
console.log(res.data.deleteProject.proj_name)
// console.log('success!');
setResultMessage(res.data.deleteProject.proj_name)
setStatusBase({
msg: `Successfully Deleted ${resultMessage}`,
key: Math.random()
})
}
const handleError = (err) => {
console.log('error')
}
//Handles the Modal for Add Project
const handleAddOpen = () => {
setAddOpen(true);
};
const handleAddClose = () => {
setAddOpen(false);
};
//Handles the Modal for Edit Project
const handleEditOpen = () => {
setEditOpen(true);
};
const handleEditClose = () => {
setEditOpen(false);
};
if (loading) return '...Loading'
if (error) return `Error: ${error.message}`
return (
<div>
<div style={{ marginTop: 20, padding: 30 }}>
<Grid container spacing={8} justify='center' alignItems='center'>
{data.projects.map(p => {
return (
<Grid item key={p._id}>
<Card >
<CardActionArea>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<CardMedia
style={{ width: 400, height: 100, paddingTop: 10, }}
component='img'
alt='Project Image'
height='140'
image={require('../../images/html-css-javascript-lg.jpg')}
/>
</div>
<CardContent >
<Typography gutterBottom variant='h5' component="h2">
{p.proj_name}
</Typography>
<Typography component='p'>
{p.description}
</Typography>
</CardContent>
</CardActionArea>
<CardActions>
<Button>
<DeleteIcon onClick={e => onDelete(p._id, e)} />
</Button>
<Button onClick={handleEditOpen}>
<Modal
open={editOpen}
onClose={handleEditClose}
closeAfterTransition
BackdropComponent={Backdrop}
className={styles.modal}
>
<Fade in={editOpen}>
<div className={styles.paper}>
<EditForm
id={p._id}
close={handleEditClose}
name={p.proj_name}
desc={p.description}
gh={p.gh_link}
live={p.live_link}
img={p.image_url}
/>
</div>
</Fade>
</Modal>
<EditIcon />
</Button>
</CardActions>
</Card>
{ status ? <AlertMessage key={status.key} message={status.msg} /> : null}
</Grid>
)
}
)}
</Grid>
<Button type='button' onClick={handleAddOpen}>Add Project</Button>
<Modal
open={addOpen}
onClose={handleAddClose}
closeAfterTransition
BackdropComponent={Backdrop}
className={styles.modal}
>
<Fade in={addOpen}>
<div className={styles.paper}>
<AddForm close={handleAddClose} />
</div>
</Fade>
</Modal>
</div>
</div >
)
}
export default DisplayProjects
And here's the form. I've destructured out the props into variables and placed them into a state object called details, so they can be overwritten and submitted to the database..
import React, { useState } from 'react'
import { useParams } from 'react-router-dom'
import { useMutation, useQuery } from '#apollo/client'
import { EDIT_PROJECT, GET_ALL_PROJECTS, GET_PROJECT_BY_ID} from '../helpers/queries'
const AddForm = (props) => {
const params = useParams()
const id = params.toString()
// console.log(id);
const [editProjectItem] = useMutation(EDIT_PROJECT)
const {loading, data, error} = useQuery(GET_PROJECT_BY_ID, {
variables: {
id
},
})
const [details, setDetails] = useState({})
if (loading) return '...Loading';
if (error) return <p>ERROR: {error.message}</p>;
if (!data) return <p>Not found</p>;
setDetails(data.projectById)
console.log(data.projectById)
const submitForm = e => {
e.preventDefault()
try {
editProjectItem({
variables: { id, proj_name, description, gh_link, live_link, image_url},
refetchQueries: [{query: GET_ALL_PROJECTS}]
})
}
catch (err) {
console.log('You Goofed')
}
// setDetails({
// proj_name: '',
// description: '',
// gh_link: '',
// live_link: '',
// image_url: ''
// })
props.close()
}
const changeDetails = (e) => {
setDetails({
...details,
[e.target.name]: e.target.value
})
}
const {_id, proj_name, description, gh_link, live_link, image_url} = details
return (
<div key = {_id}>
<h2>Edit {proj_name}</h2>
<form onSubmit = {submitForm} >
<label>
Project Name:
<input
name = 'proj_name'
value = {proj_name}
onChange = {changeDetails}
/>
</label>
<label>Description</label>
<input
name = 'description'
value = {description}
onChange = {changeDetails}
/>
<label>GitHub Link</label>
<input
name = 'gh_link'
value = {gh_link}
onChange = {changeDetails}
/>
<label>Live Link</label>
<input
name = 'live_link'
value = {live_link}
onChange = {changeDetails}
/>
<label>Preview Image</label>
<input
name = 'image_url'
value = {image_url}
onChange = {changeDetails}
/>
<button type = 'submit'>Submit</button>
</form>
</div>
)
}
export default AddForm
The problem I'm running into, is that when I access the modal, the props are sent from literally EVERY Object, instead of the one, and displays the data for the last record instead of the one I want to edit You can see what happens here (I logged props.id in order to test) https://imgur.com/a/pcEKl89
What did I miss? (Disclaimer: I am still a student, and learning the craft.. be gentle on my code please)
EDIT: I just realized that I didn't indicate that this is the final form of the EditForm component. I haven't added the logic in to make the updates yet, I just wanted to get the data showing properly first.
EDIT2: I made some changes to how the ID is passed over, I was already using React-Router, so I went ahead and made a route to /edit/:id and then using useParams(), I got the ID that way. It seems to be working, however now I'm getting a Too many re-renders message. Updated the AddForm code above to reflect the changes..
I figured out the re-render issue.. it was as simple as dropping the setDetails function into a useEffect Hook:
useEffect(()=> {
if(data){
setDetails(data.projectById)
}
},[data])
Here i am trying to set the open prop of the MUIDrawer to true when the user clicks it but while setting the state i am getting an error "Unexpected keyword 'true' "
import React, { useState } from "react";
import { withRouter } from "react-router-dom";
import {
Drawer as MUIDrawer,
ListItem,
List,
ListItemIcon,
ListItemText,
AppBar,
Toolbar,
IconButton
} from "#material-ui/core";
import { makeStyles } from "#material-ui/core/styles";
import InboxIcon from "#material-ui/icons/MoveToInbox";
import MailIcon from "#material-ui/icons/Mail";
import MenuIcon from "#material-ui/icons/Menu";
const useStyles = makeStyles({
drawer: {
width: "190px"
}
});
const Drawer = props => {
const { history } = props;
const classes = useStyles();
const itemsList = [
{
text: "Home",
icon: <InboxIcon />,
onClick: () => history.push("/")
},
{
text: "About",
icon: <MailIcon />,
onClick: () => history.push("/about")
},
{
text: "Contact",
icon: <MailIcon />,
onClick: () => history.push("/contact")
}
];
[state, setState] = useState(false);
const toggleDrawer = {setState(true)}
return (
<>
<AppBar>
<Toolbar>
<IconButton
style={{ position: "absolute", right: "0" }}
onClick={toggleDrawer}
>
<MenuIcon />
</IconButton>
</Toolbar>
</AppBar>
<MUIDrawer
className={classes.drawer}
open={state}
>
<List>
{itemsList.map((item, index) => {
const { text, icon, onClick } = item;
return (
<ListItem button key={text} onClick={onClick}>
{icon && <ListItemIcon>{icon}</ListItemIcon>}
<ListItemText primary={text} />
</ListItem>
);
})}
</List>
</MUIDrawer>
</>
);
};
export default withRouter(Drawer);
The errors are in:
[state, setState] = useState(false);
const toggleDrawer = {setState(true)}
First you forgot the const keyword in the useState hook.
const [state, setState] = useState(false);
And the toggleDrawer must be a function you can do that like the following:
const toggleDrawer = () => {setState(true)}
or
function toggleDrawer(){
setState(true)
}
If you want, you can make the function inside the onClick:
<IconButton
style={{ position: "absolute", right: "0" }}
onClick={()=>{setState(true)}}
>
And finally if you want to make it false when you click it again:
<IconButton
style={{ position: "absolute", right: "0" }}
onClick={()=>{setState(!state)}}
>
In this last case setState(!state) will allow you to save the opposite of state.
Then, for each click you make, the state value will change to the opposite of the previous value.
Try declaring toggleDrawer as a function, like this:
const toggleDrawer = () => setState(true)