React Material UI Grid Item doesn`t render after data update - reactjs

I'm unable to make following code render grid items when props.data changes from the top component.
import React from 'react';
import { makeStyles } from '#material-ui/core/styles';
import Grid from '#material-ui/core/Grid';
import ProductCard from './ProductCard';
const useStyles = makeStyles((theme) => ({
grid: {
padding: "8px",
},
}));
export default function CenteredGrid(props) {
const classes = useStyles();
const visibleProductData = props.data === null ? {} : props.data;
return (
<Grid container >
{console.log("This is from the product card grid")}
{console.log(visibleProductData)}
{Object.entries(visibleProductData).map(productData => (
<Grid key={productData[0]} className={classes.grid} item md={3} sm={6} xs={12}>
<ProductCard data={productData[1]}/>
</Grid>
))}
</Grid>
);
}
When I run this, after the data updates, the console logs visibleProductData which is a dictionary consisting of three products, as expected. However these products are not visible, in fact when I inspect I see no children for Grid container. What is weird is that, even after small changes in code, when a fast refresh occurs products become visible. What might be the issue here ?
PS: I'm using nextjs along with material ui.
Edit / Update - Parent Component
const classes = useStyles();
const { buyer, categoryData, filterData, visibleProductData } = useContext(BuyerContext);
if (!buyer) {
return (
<AuthRequired/>
)} else {
return (
<>
<HeaderBar/>
<Grid className={classes.breadcrumb} container>
<Breadcrumb />
</Grid>
<Divider variant="middle" />
<main className={classes.main}>
<Grid container>
<Grid item xs={2}>
<Box display={{ xs: 'none', sm: 'block' }}>
<CategoryList data={categoryData}/>
</Box>
</Grid>
<Grid item sm={10} xs={12}>
<FilterGrid data={filterData}/>
<ProductCardGrid data={visibleProductData}/>
</Grid>
</Grid>
</main>
<Footer/>
</>
)
}
}

Try the following line,
const visibleProductData = props.data === null ? {} : {...props.data};
It might be because your visibleProductData variable is always getting the same reference object. You need to create a new reference object each time props.data changes. If the issue still persists, then we need to see your parent component. The issue might be there.

Writing const visibleProductData = props.data === null ? {} : props.data; in React functional component body is not the correct "React way". You should:
define a local state variable called, for example, visibleProductData:
const [visibleProductData, setVisibleProductData] = useState({});
use useEffect hook to "listen" new values comes from parent object. Something like:
useEffect(() => {
setVisibleProductData(props.data === null ? {} : {...props.data});
}, [props.data]);
In this way, every time props.data changes, useEffect will be fired and it will update local visibleProductData.

I could at last solve the problem, it was a small typo that gives no error and therefore hard to debug. Instead of putting another "(" within map like so, {array.map(element => (...))} I should have done without it like this {array.map(element => ...)}.

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.

Uncaught TypeError: user.toLowerCase is not a function

okay guys so new update. so i did jus that . i moved all the elements to the rendering and tried to just get the state to be alone. when i tried that it told me that map wasnt able to read properties of undefined. so what i did was added the object keys . the page rendered ... kinda. it rendered without all the data. i attached the updated code in here but now im kinda confused because its not showing an error this time .its just not displaying the info. can someone tell me what im doing wrong here ?
The error message tells you that user.toLowerCase is not a function, indicating that user is not a string as toLowerCase only works with strings. Instead, user is the ul element you create in the setInfo(json.students.map()) call in the useEffect. It's not ideal to store DOM elements in state for this reason as it's not easy to work with. Instead, leave your info state variable as an array of objects, and move the logic for creating DOM elements in your return.
In addition, I would not change your state when you filter as you would not be able to easily get the original, unfiltered state values back. Keep track of your filters in state and do conditional rendering based on the filters in your return.
You setting your user info as a ul html list. Instead set the userInfo the json response and then map the ul list when you render the component.
I'm not sure what your user json looks like, so you might have to play around with that.
import React, { useEffect, useState } from "react";
import Card from "#material-ui/core/Card";
import CardContent from '#material-ui/core/CardContent';
import Grid from "#material-ui/core/Grid";
import { Input } from "#material-ui/core";
function StudentProfiles() {
const [info, setInfo] = useState();
const [search, setSearch] = useState('');
useEffect(() => {
fetch("https://api.hatchways.io/assessment/students")
.then(response => response.json())
.then(json => setInfo(json))
},[]);
const average = (array) => array.reduce((a,b) => a + b )/ array.length;
const filter = (e) => {
const keyword = e.target.value;
if(keyword !== '') {
const results = info.filter((user) => {
return user.students.toLowerCase().startsWith(keyword.toLowerCase());
});
setInfo(results);
} else {
setInfo(info)
}
}
return (
<div>
<Card className="card">
<CardContent className="scrollbar scrollbar-primary mt-5 mx-auto">
<Input
className="searchBar"
icon="search"
placeholder="Search by name"
onChange={filter}
/>
{ user.students.map((name) => (
<ul className = "border" key={name.id}>
<Grid item xs={3} sm={6} md={12} style={{display: "flex", gap:"3.5rem", paddingBottom:"8px"}}>
<img alt ="" src={name.pic} className="picture"></img>
<Grid container style={{display: "inline"}} align="left" justify="flex-end" alignItems="flex-start">
<Grid className="studentNames">
<span>{name.firstName + " " + name.lastName}</span>
</Grid>
<span>{name.email}</span>
<br/>
<span>{name.company}</span>
<br/>
<span>{name.skill}</span>
<br/>
<span>Average: {average(name.grades).toFixed(3)}%</span>
</Grid>
</Grid>
</ul>
)))}
</CardContent>
</Card>
</div>
)
}
export default StudentProfiles;

ReactJS MUI Component not rendering inside map function

I am using ReactJS along with Material UI Components. I am trying to render a custom element using the below code. The options array is never empty and the console logs are showing as expected - "Adding element to grid ..". However, the element is not rendered on the browser(I checked the browser Inspector to confirm).
What am I missing?
import React from "react";
import Container from "#mui/material/Container";
import Grid from "#mui/material/Grid";
const Element = (props) => {
const {options} = props;
return(
// If I simply pass a JSX component at this location, it renders fine.
// But not inside the map function
{options.map((opt) => {
opt.elements.length > 0 && (
<Grid container spacing={4}>
{opt.elements.map((el) => (
<Grid item xs={12} sm={6}>
{console.log("Adding element to grid ..", el.element_name)}
<h1>{el.element_name}</h1>
</Grid>
))}
</Grid>
);
})}
)
}
You should use parentheses instead of curly brackets inside the first map in the return()
{options.map((opt) => (
opt.elements.length > 0 && (
<Grid container spacing={4}>
{opt.elements.map((el) => (
<Grid item xs={12} sm={6}>
{console.log("Adding element to grid ..", el.element_name)}
<h1>{el.element_name}</h1>
</Grid>
))}
</Grid>
);
))}

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.

React useCallback and useEffect at the same time messed the state

I created a SignaturePad for my application that will pass the value later on to Formik.
Problem 1:
I have some issue with using the useEffect and useCallback function of react. Previously before I add useEffect, handleClear function works just fine. However after I added useEffect, sigPad refs always returns null in handleClear.
I'm not sure if my mental model is correct, can anyone please explain why is this happening?
Problem 2:
I realised however, after I removed the [sigPad] at handleClear I am able to receive the ref again. Which part of my code re-renders and how does useCallback not realise that sigPad is changing from null to the correct ref?
Problematic code:
export function Signature() {
let sigPad = useRef(null);
const [sig, setSig] = useState("");
const classes = formStyles();
useEffect(() => {
console.log(sigPad);
setSig(sigPad.toData());
console.log(sigPad);
}, [sig]);
const handleClear = useCallback(() => {
console.log(sigPad);
if (sigPad) sigPad.clear();
}, [sigPad]);
return (
<div className="row">
<Grid spacing={3} container>
<Grid item xs={12}>
<h3 className="text-bold">Signature</h3>
</Grid>
<Grid item xs={12}>
<div className="sigCanvas">
<SignatureCanvas
penColor="black"
canvasProps={{ className: "sigPad" }}
ref={ref => {
sigPad = ref;
}}
/>
</div>
</Grid>
<Grid item xs={3}>
<Button
variant="contained"
component="label"
className={classes.instructions}
onClick={handleClear}
>
Clear
</Button>
</Grid>
<Grid item xs={3}>
<Button
component="label"
variant="contained"
className={classes.instructions}
>
Save
</Button>
</Grid>
</Grid>
</div>
);
}
Current Fix:
const handleClear = useCallback(() => {
console.log(sigPad);
if (sigPad) sigPad.clear();
});
Note: Before I add useEffect I don't have to remove the [sigPad] at my callback
Hooks are executed in the order they are called, this means that your useEffect takes precedence over your useCallback function. Also, this [] is called the dependency array and is used to create new instances of the underlying structure. When using useRef you don't need to do this
ref={ref => { sigPad = ref; }}
You can just do
ref={sigPad}
And try moving your useCallback above useEffect.

Resources