0
votes

I am using an API to fetch and render the data it into cards. I want to render a corresponding modal for each card when the card is clicked. I am using the React modal component by material-ui (the simple modal example). The problem is that instead of the modal for the clicked card, I'm getting the modal for the last card. I managed to "capture" the data of the clicked card in a state hook, but I'm not sure how to use that to render the correct component.

This is the Home component where the cards are rendered:

import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { BeerCardExpanded } from './BeerCardExpanded';
import { BeerCard } from './BeerCard';

import { makeStyles } from '@material-ui/core/styles';
import Modal from '@material-ui/core/Modal';

import '../styles/Home.css';
import { DialogContent } from '@material-ui/core';

function rand() {
  return Math.round(Math.random() * 20) - 10;
}

function getModalStyle() {
  const top = 50 + rand();
  const left = 50 + rand();

  return {
    top: `${top}%`,
    left: `${left}%`,
    transform: `translate(-${top}%, -${left}%)`,
  };
}

const useStyles = makeStyles(theme => ({
  paper: {
    position: 'absolute',
    width: 400,
    backgroundColor: theme.palette.background.paper,
    border: '2px solid #000',
    boxShadow: theme.shadows[5],
    padding: theme.spacing(2, 4, 3),
  },
}));

export const Home = () => {
  const ref = React.createRef();
  const classes = useStyles();
  const [modalStyle] = React.useState(getModalStyle);
  const [beers, setBeers] = useState([]);
  const [open, setOpen] = useState(false);
  const [isClicked, setIsClicked] = useState([]);

  const fetchBeerData = async () => {
    try {
      const { data } = await axios.get(
        'https://api.punkapi.com/v2/beers?per_page=9'
      );
      console.log(data);
      return data;
    } catch (err) {
      console.log(err);
    }
  };

  useEffect(() => {
    fetchBeerData().then(data => {
      setBeers(data);
    });
  }, []);

  const handleOpen = id => {
    setIsClicked(isClicked.push(beers.filter(item => item.id === id)));
    setIsClicked(id);
    setOpen(true);
    console.log(isClicked[0]);
  };

  const handleClose = () => {
    setOpen(false);
    setIsClicked([]);
  };

  return (
    <div className='beer-container'>
      {beers.map((beer, index) => (
        <>
          <BeerCard
            key={beer.name}
            beer={beer}
            id={index}
            handleOpen={handleOpen}
          />
          <Modal
            aria-labelledby='transition-modal-title'
            aria-describedby='transition-modal-description'
            open={open}
            onClose={handleClose}
          >
            <DialogContent>
              <BeerCardExpanded
                id={`${isClicked.id}-${isClicked.name}`}
                className={classes.paper}
                style={modalStyle}
                beer={beer}
                ref={ref}
              />
            </DialogContent>
          </Modal>
        </>
      ))}
    </div>
  );
};

This is the BeerCard component:

import React, { useState } from 'react';
import '../styles/BeerCard.css';
import CardActions from '@material-ui/core/CardActions';
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder';
import FavoriteIcon from '@material-ui/icons/Favorite';
import IconButton from '@material-ui/core/IconButton';

export const BeerCard = ({ beer, handleOpen }) => {
  const [isFavorite, setIsFavorite] = useState(false);

  const handleIconClick = e => {
    e.stopPropagation();
    setIsFavorite(!isFavorite);
  };

  return (
    <div className='card' onClick={() => handleOpen(beer.id)}>
      <div className='image-container'>
        <img src={beer.image_url} alt={beer.name} />
      </div>

      <div className='info-container'>
        <section className='name-tagline'>
          <p className='beer-name'>{beer.name}</p>
          <p className='beer-tagline'>
            <i>{beer.tagline}</i>
          </p>
        </section>
        <p className='beer-description'>{beer.description}</p>
      </div>
      <CardActions className='favorite-icon'>
        <IconButton aria-label='add to favorites'>
          {!isFavorite ? (
            <FavoriteBorderIcon onClickCapture={e => handleIconClick(e)} />
          ) : (
            <FavoriteIcon onClickCapture={e => handleIconClick(e)} />
          )}
        </IconButton>
      </CardActions>
    </div>
  );
};

And the BeerCardExpanded is the component that gets injected in the material-ui's modal component:

import React, { useState } from 'react';
import CardActions from '@material-ui/core/CardActions';
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder';
import FavoriteIcon from '@material-ui/icons/Favorite';
import IconButton from '@material-ui/core/IconButton';

import '../styles/BeerCardExpanded.css';

export const BeerCardExpanded = React.forwardRef(({ beer }, ref) => {
  const [isFavorite, setIsFavorite] = useState(false);

  const handleIconClick = e => {
    e.stopPropagation();
    setIsFavorite(!isFavorite);
  };

  return (
    <div className='beer-card' ref={ref}>
      <CardActions className='favorite-icon'>
        <IconButton aria-label='add to favorites'>
          {!isFavorite ? (
            <FavoriteBorderIcon onClickCapture={e => handleIconClick(e)} />
          ) : (
            <FavoriteIcon onClickCapture={e => handleIconClick(e)} />
          )}
        </IconButton>
      </CardActions>

      <section className='top'>
        <section className='image-name-tagline'>
          {<img src={beer.image_url} alt={beer.name} />}
          <section className='right-side'>
            <p className='beer-name'>{beer.name}</p>
            <p className='beer-tagline'>
              <i>{beer.tagline}</i>
            </p>
          </section>
        </section>
        <p className='beer-description'>{beer.description}</p>
      </section>

      <section>
        <p className='food-pairing'>
          <i>
            Pairs best with:
            {beer.food_pairing.map((item, index) => (
              <span key={(item, index)}> ☆{item} </span>
            ))}
          </i>
        </p>
      </section>
    </div>
  );
});

I created a codesandbox, here's the link.

1
I'm unable to run this to verify, however it appears you issue is causd by Modal being inside the map (beers.map). Try moving the Modal ouside. It would be very helpful to move this code into something like jsFiddle / code pen.Nathan Hall
I created a codesandbox and provided the link at the bottom of my post. I had tried to move the Modal outside of the map method. I changed the value of the "beer" prop from beer to isClicked[0], but I'm getting a "TypeError: Cannot read property 'image_url' of undefined" when I click on a card. The error is referencing the BeerCardExpanded component.StelKizi
Thanks! This was big help, see answer belowNathan Hall

1 Answers

0
votes

Looks like you were getting the beer.id mixed up with the map index. Try using the Id rather than index.

      {beers.map((beer) => (
          <BeerCard
            key={beer.name}
            beer={beer}
            id={beer.id}
            handleOpen={handleOpen}
          />
      ))}

https://codesandbox.io/s/new-tree-z3pxf

Notice the filter inside handleOpen also

   useEffect(() => {
    const fetchBeerData = async () => {
      try {
        const { data } = await axios.get(
          "https://api.punkapi.com/v2/beers?per_page=9"
        );
       setBeers(data);
      } catch (err) {
        console.log(err);
      }
    };
    fetchBeerData();
  }, []);

  const handleOpen = (id) => {
    setIsClicked(beers.find(x => x.id === id));
    setOpen(true);
  };

  const handleClose = () => {
    setOpen(false);
    setIsClicked({});
  };

  return (
    <div className="beer-container">
      {beers.map((beer) => (
          <BeerCard
            key={beer.name}
            beer={beer}
            id={beer.id}
            handleOpen={handleOpen}
          />
      ))}

      <Modal
        aria-labelledby="transition-modal-title"
        aria-describedby="transition-modal-description"
        open={open}
        onClose={handleClose}
      >
          <DialogContent>
            <BeerCardExpanded
              id={`${isClicked.id}-${isClicked.name}`}
              className={classes.paper}
              style={modalStyle}
              beer={isClicked}
              ref={ref}
            />
          </DialogContent>
      </Modal>
    </div>
  );