Setting Active State on mapped component - reactjs

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.

Related

Make sure state is updated before rendering data in React Modal

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.

How to change to week and days according to current views

I am customizing react big calendar toolbar, and it's working fine for switching month, but the issue is i am unable to switch week and days according to the current views.
In every views next , prev button change the month.
This is custom toolbar for navigating
const CustomToolbar = (toolbar, open, setOpen) => {
const goToBack = () => {
toolbar.date.setMonth(toolbar.date.getMonth() - 1);
toolbar.onNavigate("prev");
};
const goToNext = () => {
toolbar.onNavigate("next");
toolbar.date.setDays(toolbar.date.getDay() + 1);
//i want to change the dates state according to current view
};
const goToWeekView = () => {
toolbar.onView("week");
};
const goToMonthView = () => {
toolbar.onView("month");
};
const goToDayView = () => {
toolbar.onView("day");
};
const goToAgendaView = () => {
toolbar.onView("agenda");
};
return (
<Stack>
<Box className="back-next-buttons">
<Stack direction="row" alignItems="center">
<IconButton onClick={goToBack} className="btn-back">
<ArrowBackIosIcon />
</IconButton>
<Typography
onClick={goToCurrent} >
Today
</Typography>
<IconButton onClick={goToNext} className="btn-back">
<ArrowForwardIosIcon/>
</IconButton>
</Stack>
</Box>
{/* date label */}
<Typography>
{label()}
</Typography>
{/* view buttons */}
<Stack direction="row" flexWrap="wrap" alignItems="center">
<SmallButton
onClick={goToWeekView}
label="Week"/>
<SmallButton
onClick={goToMonthView}
label="Month"/>
<SmallButton
onClick={goToDayView}
label="Day"/>
<SmallButton
onClick={goToAgendaView}
label="All Events"/>
</Stack>
</Stack>
);
};
When building custom components for Big Calendar it is helpful to look at the source code of the actual components, or of components used in the documentation. Looking at the source of the custom toolbar in the components prop, you'll see that the toolbar is passed the internal onNavigate method, and uses the exported {navigate} constants to help you navigate to the proper views.
The internal onNavigate method is not well documented (yet), but is very easy to work with.
const today = () => onNavigate(navigate.TODAY);
const next = () => onNavigate(navigate.NEXT);
const previous = () => onNavigate(navigate.PREVIOUS);
// must be JS Date, and only if controlling `date` prop
const toThisDay = (newDate) => onNavigate(navigate.DATE, newDate);
If you happen to be controlling view and date in the same method, then setting both simultaneously can be a challenge due to the internal logic of onView and state rerendering, and may require you to wait a tic between setting each variable.
const toThisDay = (newDate) => {
onView('day');
window.setTimeout(() => {
onNavigate(navigate.DATE, newDate);
}, 100);
};

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

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 => ...)}.

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.

Create separate state for similar react children object in an array

I have created a small social media app where there is a like button to like the post. When I press like on first button, second post's like changes color too which is state based.
Here is the code:
Likes.js
<LikedPanel
posts_id={posts_id}
person_id={person_id}
toggleLikePost={this.toggleLikePost}
liked={this.state.liked}
/>
const LikedPanel = ({posts_id, person_id, liked, toggleLikePost}) => (
<Panel onClick={() => toggleLikePost(posts_id, person_id)}>
{liked
?
<Fragment>
<LikeIcon nativeColor={indigo[500]}/>
<LikedText>Like</LikedText>
</Fragment>
:
<Fragment>
<LikeIcon/>
<LikeText>Like</LikeText>
</Fragment>
}
</Panel>
);
toggleLikePost = (posts_id, person_id) => {
// make api call and then setState
this.setState(liked: res.data.liked);
};
Update
Posts.js
const Posts = ({ feed, deletePost }) => (
<Card style={{ margin: '25px 0'}} square>
<ContentMedia post={feed.post} deletePost={deletePost} />
<Like liked={feed.post.likes.length > 0} posts_id={feed.post.id} person_id={feed.post.person_id} />
<Comments post_id={feed.post.id} comments={feed.post.comments} />
</Card>
);
Newsfeed.js
<div style={{ maxWidth: '500px', margin: '0 auto', paddingTop: '20px'}}>
{this.renderPosts(this.state.feed)}
</div>
renderPosts = (feeds) =>
feeds.map(feed => <Posts key={feed.post.id} feed={feed} deletePost={this.deletePost} />);
It loads fine when I refresh. But when I click like on any one post, all the other like icon gets toggled and vice versa. I see that it uses the same state for all elements. Is there a way to tell react to have its own self-contained state for every post's like panel?
Instead of storing a boolean value liked that it used by all the like buttons to determine if they should be toggled you can store an array of the liked posts. Then you can add liked post id to that array and detect in your LikedPanel if the current post id is in the liked array.
<LikedPanel
posts_id={posts_id}
person_id={person_id}
toggleLikePost={this.toggleLikePost}
liked={this.state.liked.indexOf(posts_id) !== -1 }
/>
toggleLikePost = (posts_id, person_id) => {
// make api call and then setState
this.setState(prevState => {
// add post id to liked array only if it's not added yet
if (prevState.liked.indexOf(posts_id) === -1) {
return {liked: [...prevState.liked, posts_id]};
}
});
};

Resources