Search code examples
reactjsmaterial-uionclickuse-statecard

How to target single item in list with onClick when mapping in ReactJS?


My react component returns data from my Firestore DB and maps the data it on Material-UI cards. However, when I press the ExpandMoreIcon, it opens EVERY card. I just want to open each card individually. I know the solution has to do with useState function for expanded & setExpanded.

I've tried to fix this bug but I cant seem to make it work. Any help would be greatly appreciated.

export const NFTprojects = () => {

const [expanded, setExpanded] = useState(false);

const handleExpandClick = (id) => {
    setExpanded(!expanded)
};

const [projects, setProjects] = useState([]);

const ref = firebase.firestore().collection("NFTprojects");

function getProjects() {
    ref.onSnapshot((querySnapshot) => {
        const items = []; //initiliaze empty array
        querySnapshot.forEach((doc) => {
            items.push(doc.data());
        });
        setProjects(items);
    });
}

useEffect(() => {
    getProjects();
}, []);


return (
    <div>
        <Grid container spacing={4} direction="row" justifyContent="flex-start" alignItems="flex-start">

            {projects.map((project) => (
                <Grid item xs={4}>
                    <Card sx={{ maxWidth: 400, borderRadius: 3, mb: 5 }}>
                        <CardMedia
                            component="img"
                            height="140"
                            image={project.imageUrl}
                            alt={project.projectName}
                        />
                        <CardContent>
                            <Typography gutterBottom variant="h5" sx={{ fontWeight: 'bold' }}>
                                {project.projectName}
                            </Typography>
                            <Typography variant="h6" gutterBottom component="div" fontWeight="bold">
                                {project.jobPosition}
                            </Typography>
                            <Typography variant="body2" color="text.secondary" style={{ fontFamily: 'Merriweather' }}>
                                {project.projectDesc}
                            </Typography>
                        </CardContent>
                        <CardActions disableSpacing>

                            <Tooltip title="Website">
                                <IconButton aria-label="secondary marketplace" href={project.websiteLink} target="_blank">
                                    <WebsiteIcon />
                                </IconButton>
                            </Tooltip>

                            <Tooltip title="Twitter">
                                <IconButton aria-label="twitter" href={project.twitterLink} target="_blank">
                                    <TwitterIcon />
                                </IconButton>
                            </Tooltip>

                            <Tooltip title="Secondary">
                                <IconButton aria-label="Secondary market link" href={project.secondaryMarket} target="_blank">
                                    <ShoppingCartIcon />
                                </IconButton>
                            </Tooltip>

                            <Tooltip title="Discord">
                                <IconButton aria-label="discord" href={project.discordLink} target="_blank">
                                    <SvgIcon component={DiscordIcon} viewBox="0 0 600 476.6" />
                                </IconButton>
                            </Tooltip>


                            <Button size="small" variant="contained" sx={{ ml: 15, backgroundColor: 'black' }}>Apply</Button>


                            <ExpandMore
                                expand={expanded}
                                onClick={handleExpandClick}
                                aria-expanded={expanded}
                                aria-label="show more"
                            >
                                <ExpandMoreIcon />
                            </ExpandMore>

                        </CardActions>

                        <Collapse in={expanded} timeout="auto" unmountOnExit>
                            <CardContent>
                                <Typography variant="h6" sx={{ fontWeight: 'bold' }} style={{ fontFamily: 'Merriweather' }}>Job Description:</Typography>
                                <Typography paragraph>
                                    {project.jobDesc}
                                </Typography>

                                <Typography variant="h6" sx={{ fontWeight: 'bold' }}>Prerequisites</Typography>
                                <Typography paragraph>
                                    {project.jobPrereq}
                                </Typography>

                            </CardContent>
                        </Collapse>

                    </Card>
                </Grid>
            ))}
        </Grid>
    </div >
);

}


Solution

  • One approach is to create a separate component for the card. This will enable you to add states to the component and control them. Here is a minimal example demonstrating how you can approach it.

    import React, { useState } from "react";
    
    // this is just sample data to work with - equivalent to the data you get from Firebase
    const sampleCardsArray = [
      {
        id: 0,
        name: "Card 1",
        color: "red",
        description: "This is card 1",
      },
      {
        id: 1,
        name: "Card 2",
        color: "blue",
        description: "This is card 2",
      },
      {
        id: 2,
        name: "Card 3",
        color: "green",
        description: "This is card 3",
      },
    ];
    
    // component for all cards
    export const AllCards = () => {
        // this state is used to store the INDEX of the card that is currently expanded
      const [expandedCard, setExpandedCard] = useState(null);
      return (
        <div>
          {sampleCardsArray.map((card, index) => (
            <OneCard
              card={card}
              key={card.id}
              // this prop passes the boolean value of whether the card is expanded or not
              isExpanded={expandedCard === index}
              // this prop receives the index of the card that is expanded and sets the state
              expandCard={() => setExpandedCard(index)}
            />
          ))}
        </div>
      );
    };
    
    // component for one card
    // We only show the fields: name and color. We show the description when the card is clicked
    export const OneCard = ({ card, isExpanded, expandCard }) => {
      return (
        <div>
          <h1>{card.name}</h1>
          <h2>{card.color}</h2>
    
          {
            // showing expand button only when card is not expanded
          }
          {isExpanded ? (
            <p>{card.description}</p>
          ) : (
            <button onClick={() => expandCard()}>Expand card</button>
          )}
        </div>
      );
    };