3

New to programming so I'm sorry if I'm not wording this correctly. I'm using a .map to render and list every single item on an array. For each item, I want the modal to open/close only the specific modal corresponding to each item in the array. However, when I click on the button to open the modal, every single one opens and closes. I believe this is because the modals are all set to an on/off button together. How can I set it it (with the .map value.id or something) so that only the specific modal opens and closes?

class DrinkMenu extends Component {
    constructor(props, context) {
        super(props, context);
        this.state = {
            show: false
        };
        this.handleHide = this.handleHide.bind(this);
    }

    handleHide() {
        this.setState({ show: false });
    }

    async componentDidMount() {
        let res = await axios.get('/getAllDrinks')
        this.props.updateDrinkMenu(res.data)
    }

    async addToCart(drinkObj) {
        let res = await axios.post('/addToCart', drinkObj)
        console.log(`Added ${drinkObj.name} to order.`)
    }

    render() {
        let drinkList = this.props.drinkMenu.map((drink) => {
            return (
                <div key={drink.id}>
                    <h5>{drink.name}</h5>
                    <h6>${drink.price}</h6>

                    <span
                        onClick={() => this.setState({ show: true })}
                    >
                        <strong>More Info</strong>
                        <br />
                        <button onClick={() => this.addToCart(drink)}>Add to Order</button>

                    </span>

                    <Modal
                        show={this.state.show}
                        onHide={this.handleHide}
                        container={this}
                        aria-labelledby="contained-modal-title"
                    >
                        <Modal.Header closeButton>
                            <Modal.Title id="contained-modal-title">
                                {drink.name}
                            </Modal.Title>
                        </Modal.Header>
                        <Modal.Body>
                            <p>{drink.sub_category} | ABV {drink.abv}% | {drink.origin}</p>
                            <Col xs={6} md={4}>
                                <Image className="drink-logo" src={drink.logo} thumbnail />
                            </Col>
                            <p className="drink-description"><strong>Description</strong><br />{drink.description}</p>
                            <p href={drink.website}>Website</p>
                        </Modal.Body>
                        <Modal.Footer>
                            <Button onClick={this.handleHide}>Close</Button>
                        </Modal.Footer>
                    </Modal>
                </div>
            )
        })

        return (
            <div>
                <h2>Drink Menu</h2>
                <div>
                    {drinkList}
                </div>
            </div>
        )
    }
}
Pranay Tripathi
  • 1,614
  • 1
  • 16
  • 24
Justin C.
  • 109
  • 1
  • 9

1 Answers1

3

From the code you have shared, I see that you are handling all the Model with the same state value, i.e. show. This is causing all the state for all the Models to be true hence all of them as shown.

To solve this, you can extract your whole component in a new React class which has just the functionality to show Modal as per the independent state. So your new React component will look something like this:

class DrinkComponent extends React.Component {
    constructor(props) {
        super(props);
        this.handleHide = this.handleHide.bind(this);
        this.state = {
            show: false,
        }
    }
    handleHide() {
        this.setState({ show: false });
    }
    render() {
        const { drink } = this.props;
        return (<div key={drink.id}>
            <h5>{drink.name}</h5>
            <h6>${drink.price}</h6>
            <span
                onClick={() => this.setState({ show: true })}
            >
                <strong>More Info</strong>
                <br />
                <button onClick={() => this.props.addToCart(drink)}>Add to Order</button>
            </span>
            <Modal
                show={this.state.show}
                onHide={this.handleHide}
                container={this}
                aria-labelledby="contained-modal-title"
            >
                <Modal.Header closeButton>
                    <Modal.Title id="contained-modal-title">
                        {drink.name}
                    </Modal.Title>
                </Modal.Header>
                <Modal.Body>
                    <p>{drink.sub_category} | ABV {drink.abv}% | {drink.origin}</p>
                    <Col xs={6} md={4}>
                        <Image className="drink-logo" src={drink.logo} thumbnail />
                    </Col>
                    <p className="drink-description"><strong>Description</strong><br />{drink.description}</p>
                    <p href={drink.website}>Website</p>
                </Modal.Body>
                <Modal.Footer>
                    <Button onClick={this.handleHide}>Close</Button>
                </Modal.Footer>
            </Modal>
        </div>);
    }
}

In this case, each DrinkComponent will have its independent state of showing and hiding of the model. Now we have to just modify your existing render function in DrinkMenu, to display DrinkComponent. So your render function will look something like this:

render() {
        let drinkList = this.props.drinkMenu.map((drink) => (<DrinkComponent drink={drink} addToCart={this.addToCart}/>));
        return (
            <div>
                <h2>Drink Menu</h2>
                <div>
                    {drinkList}
                </div>
            </div>
        )
    }

Also you can remove the show state from DrinkMenu as it wont be needed there. Hope it helps.

Pranay Tripathi
  • 1,614
  • 1
  • 16
  • 24
  • 1
    That fixed it! Thank you so much! Really appreciate your help! – Justin C. Nov 22 '18 at 01:25
  • 2
    @JustinC. A side note about your `componentDidMount`... it probably shouldn't be asynchronous, but since axios returns promises, they are 'then'-able. componentDidMount() { axios.get('/getAllDrinks') .then(function(res) { this.props.updateDrinkMenu(res.data); }); } As a second aside, it's odd that this component is calling out to update the drink menus it is also rendering from props. It's typically calculated by a parent component and then passed as you've done. – Drew Reese Nov 22 '18 at 01:42
  • 1
    @JustinC please accept the answer in case it has resolved the issue – Pranay Tripathi Nov 22 '18 at 02:06
  • @DrewReese Hi Drew. Between using async/await, or just the .then, is there one method that is better than the other, or considered a better practice? In regards to the updateDrinkMenus, I'm also using Redux, and this is updating the Redux state so I can map through all of the data and display it. Once again I'm not sure if I'm doing this the best way, so if you have any tips, that would be appreciated! – Justin C. Nov 22 '18 at 18:00
  • 1
    @JustinC. Functionally async/await and promises are two different things, but commonly work together. The key aspect to note here is that the javascript thread will pause at the `await` until that line finishes executing before continuing within that function. With the promise however, the resolution/rejection is put into a queue to be asynchronously executed later, so the rest of the function executes. In this case with react, you don't want to needlessly pause its lifecycle functions. – Drew Reese Nov 22 '18 at 18:28
  • 1
    Updating redux state should happen when the app loads, or when the parent container is getting ready to render the list. The parent should be responsible for getting the up-to-date drink options, then pass those to the list through a `mapStateToProps` redux function. – Drew Reese Nov 22 '18 at 18:31
  • @PranayTripathi My drinkList currently maps through the entire list of drinks that I have in my database. If I wanted the option to break it down into sub-categories, how could I go about doing this? I currently have something like: render() { let lagers = this.props.drinkMenu.filter(drink => drink.category === 'lagers').map((drink) => ()); return (

    Lagers

    {lagers}
    ) }
    – Justin C. Nov 29 '18 at 03:46
  • @PranayTripathi However, I have about 12 different subcategories, so breaking it down like this would end up being a bit repetitive and not "DRY". Do you have any recommendations for better ways to approach this? For reference, I'm trying to create something like this: https://www.yardhouse.com/menu-listing/beer – Justin C. Nov 29 '18 at 03:48
  • 1
    well I dont see the logic changing until you change the structure of the data. – Pranay Tripathi Nov 29 '18 at 10:44