Make sure state is updated before rendering data in React Modal - reactjs

I am using Material-UI component, useState hook and NextJS framework.
I am mapping someData in my render, the structure of a someData element is :
someData[i] : {
"id": number,
"name": string,
"surname": string
}
My problem is to pass mapped data to Modal (specific to the mapped data).
{someData.map((data) => (
<Grid item key={data.id}>
<Card>
<CardContent>
<Typography>
{data.surname} {data.name}'s card.
</Typography>
</CardContent>
<Button
onClick={() => {
setModalData(data);
setOpen(true);
}}
>
Follow
</Button>
<Modal
open={open}
onClose={setOpen(false)}
>
<Box>
<Typography>
Follow {modalData.surname} ?
</Typography>
<Button onClick={() => handleFollowSubmit(modalData)}>
/*Function declared not important here*/
Yes
</Button>
</Box>
</Modal>
</Card>
</Grid>
))}
And the state used here are:
const [open, setOpen] = useState(false); // to handle the Modal opening
const [modalData, setModalData] = useState(null); // to pass Data to the modal
The idea is that you can't pass mapped data to a modal using the mapping, you have to use a State Hook to do so: When you open a modal, you pass the corresponding data through the State Hook.
But when I render the webpage I get this error :
TypeError: Cannot read properties of null (reading 'surname')
Pointing at modalData.surname
Any help would be appreciated!

Just add !!modalData to its open prop?
<Modal
open={!!modalData && open}
onClose={setOpen(false)}
/>

Update:
Initializing modalData like this:
const [modalData, setModalData] = useState({"id": number, "name": string, "surname": string})
solves the problem. I think initializing it with null created an error as the components are rendered before the data is fetched.

Related

Setting Active State on mapped component

I have a mapped component which iterates through API data. It passes props to each one and therefore each card looks different. See example below.
https://gyazo.com/39b8bdc4842e5b45a8ccc3f7ef3490b0
With the following, I would like to achieve two goals:
When the component is selected, it uses state to STAY SELECTED, and changes the colour as such to lets say blue for that selected component.
I hope this makes sense. How do I index a list as such and ensure the colour and state remains active based on this selection?
See below.
The level above, I map the following cards using these props.
{
jobs.length > 0 &&
jobs.map(
(job) =>
<JobCard key={job.id} job={job}
/>)
}
I am then using the following code for my components:
const JobCard = ({ job }) => {
const responseAdjusted = job.category.label
const responseArray = responseAdjusted.split(" ")[0]
return (
<CardContainer>
<CardPrimary>
<CardHeader>
<CardHeaderTopRow>
<Typography variant = "cardheader1">
{job.title}
</Typography>
<HeartDiv>
<IconButton color={open ? "error" : "buttoncol"} sx={{ boxShadow: 3}} fontSize ="2px" size="small" fontSize="inherit">
<FavoriteIcon fontSize="inherit"
onClick={()=> setOpen(prevOpen => !prevOpen)}/>
</IconButton>
</HeartDiv>
</CardHeaderTopRow>
<Typography variant = "subtitle4" color="text.secondary">
{job.company.display_name}
</Typography>
</CardHeader>
<CardSecondary>
</CardSecondary>
</CardPrimary>
</CardContainer>
)
}
export default JobCard
My suggestion is to use a state in the wrapping component that keeps track of the current open JobCard.
const [openCard, setOpenCard] = useState()
and then pass this down to job card together with a function to update.
jobs.map(
(job) =>
<JobCard
key={job.id}
job={job}
isSelected={openCard === job.id}
onSelectCard={() => setOpenCard(job.Id)}
/>)
So now you can format your JobCard differently depending on isSelected, and run onSelectCard when the card is pressed.

How to keep MUI accordion from expanding?

i'm using two components from MUI, accordion and Modal. I have the Modal on the accordion summary, but when i open it, everytime i click inside the modal, the accordion also receives the interaction, thus it expands. I passed the open state of Modal to the Accordion, and tried changing things based on the open state. Like
pointerEvents:`${open == true? 'none': 'auto'}`
Tried something like this with disabled, but it remains the same. I have a sample of the code here
https://stackblitz.com/edit/react-ts-zer8p7?file=accordion.js
Thanks in advance
The issue here is that your click event is bubbling up the component tree until it is consumed by the accordion summary. You can stop this anywhere in the hierarchy by using the event stopPropagation function. You could change your BaseModal component to something like so:
export default function BasicModal() {
const [open, setOpen] = React.useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const onModalClick = (e) => {
e.stopPropagation();
};
return (
<div onClick={onModalClick}>
<Button onClick={handleOpen}>Open modal</Button>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={style}>
<Typography id="modal-modal-title" variant="h6" component="h2">
Text in a modal
</Typography>
<Typography id="modal-modal-description" sx={{ mt: 2 }}>
<TemporaryDrawer />
</Typography>
</Box>
</Modal>
</div>
);
}
The onModalClick receives the click event and stops it from propagating up the component tree to the accordion summary.

Observed this Error: Material-UI: The `anchorEl` prop provided to the component is invalid

Observed the error when tried to open a popper in grid table. Error Details
Material-UI: The anchorEl prop provided to the component is invalid.
The anchor element should be part of the document layout.
Make sure the element is present in the document or that it's not display none.
below is sample code I am using in grid table:
<>
<MoreVertIcon
ref={anchorRef}
aria-controls={open ? 'menu-list-grow' : undefined}
aria-haspopup="true"
// key={uuidv4()}
onClick={handleToggle}
style={{ color: theme.palette.primary.main }}
/>
<Popper open={open} anchorEl={anchorRef.current} role={undefined} transition disablePortal>
hello world
</Popper>
</>
found a reference but not sure where I am breaking this norm. Any help is appreciated.
Finally got the reason, child component was re-rendering because I added dynamic key on map iteration which were causing props to change, as I used iteration index as key issue resolved.
Another way that this error can occur is when using a react ref. There can be a 'race condition' where the element is created before it is rendered, it can be solved by delaying setting the anchor by a frame:
const MyComponent = () => {
const anchorRef = React.useRef()
const [anchorEl, setAnchorEl] = React.useState()
React.useEffect(() => {
setTimeout(() => setAnchorEl(anchorRef?.current), 1)
}, [anchorRef])
return <div ref={anchorRef}>
{anchorEl && <Popper anchorEl={anchorEl}>hello world</Popper>}
</div>
}
In my case the issue was due to the fact my button IconButton was not receiving the actual ref={anchorRef}. Yep, that silly issue...
Also, in my case, I could do well with a name key other than an index like:
{optionsArray.map(option => (
<MenuItem key={option} onClick={handleClose}>
{option}
</MenuItem>
))}

how to update state to wait for a function to complete in React

I am developing a React functional component for a model CRUD operations, this component will render save and delete buttons on the model form, and I am trying to show a waiting indicator when the user clicks the save or delete button and hide the indicator when the process completes.
I am using the material-ui React components library, and for the waiting indicator I am using the Backdrop component.
the component props are the save and delete callbacks and set by the parent component.
I added a boolean state to show/hide this backdrop, but the waiting indicator is not showing as the setState in react is asynchronous. so how can I achieve this?
here is my component:
export default function ModelControls({onSave, onDelete}) {
const [wait, setWait] = useState(false);
const saveClick = () => {
setWait(true);
const retId = onSave();
setWait(false);
...
};
return (
<Container maxWidth={"xl"}>
<Grid container spacing={2}>
<Grid item xs={6}>
<Box display="flex" justifyContent="flex-end">
<Box component="span">
<Button size="small" color="primary" onClick={saveClick}>
<SaveIcon />
</Button>
</Box>
</Box>
</Grid>
</Grid>
<Backdrop open={wait}>
<CircularProgress color="primary" />
</Backdrop>
</Container>
);
}
Just make the function async and add await in front of the save function.
const saveClick = async () => {
setWait(true);
const retId = await onSave();
setWait(false);
};
Thanks #Dipansh, you inspired me to the following solution.
now the onSave callback from parent must return a promise object
const saveClick = () => {
setWait(true);
onSave().then((retId) => {
...
setWait(false);
});
};
this way it is working as needed.

handleExpand function is changing multiple Material UI cards in the grid

My coworker has created a grid using Material UI; each row in the grid has 3-5 Material UI cards, and each card needs to have an "expand" option to show more detail. For each row in the grid, we're using redux/hooks to pull in data; each record has the same fields (e.g. each record might have a "name", "year", etc. field). The issue we're running into is that when we expand the "name" card on one row of the grid, it expands all "name" cards in the grid. I've been trying to find a solution, but haven't come up with anything. Here's the link to the codesandbox with sample data:
https://codesandbox.io/s/inspiring-stallman-jtjss?file=/src/App.js
Each card-container that you have should implement it's own expand/collapse functionality.
You can create a new component that wraps specific card (for example <CardWrapper />) and that component will have it's own state (expandedName, setExpandedName) and so on.
A quick and dirty solution might look like this:
const CardWrapper = (dataExample) => {
const dispatch = useDispatch();
[expandedName, setExpandedName] = useState(false);
const handleExpandClickName = () => {
setExpandedName(!expandedName)
};
return (
<div className={classes.root}>
<Grid>
<Card>
<CardActions disableSpacing>
<IconButton
key={dataExample.key}
className={clsx(classes.expand, {
[classes.expandOpen]: expandedName,
})}
onClick={() => dispatch(handleExpandClickName)}
aria-expanded={expandedName}
aria-label="show more"
>
<ExpandMoreIcon />
</IconButton>
</CardActions>
<Collapse in={expandedName} timeout="auto" unmountOnExit>
<CardContent>
<Typography paragraph>Test</Typography>
</CardContent>
</Collapse>
</Card>
</Grid>
</div>);
}
And inside your code you should use something like this:
const ServiceAreaTile = () => {
const psOverviewSA = useSelector((state) => state.psOverviewSA);
return psOverviewSA.map((dataExample) => {
return (<CardWrapper dataExample={dataExample} />);
}
}
This way the expand state is being kept internally for each CardWrapper, and they don't have any collisions between them.

Resources