ReactJs mapping array of items, onClick button always returns the last item of the array

Viewed 40

I have this component and I want to be able to call a function with a given item id, but the problem is that when I call cancelItem with given parameter, it always returns the id of the last item from the array. I think it has something to do with the scope, but I cannot think of a solution.

Edit 1: Uploaded a minimal reproducible example

import * as React from "react";
import {
  Typography,
  Button,
  Dialog,
  DialogActions,
  DialogContent,
  DialogContentText,
  DialogTitle,
  ListItem,
  ListItemText,
  ListItemButton,
  Box
} from "@mui/material";

interface IItem {
  item_id: number;
  name: string;
}

interface ItemEditModel {
  itemId: number;
}

let items: IItem[] = [
  { item_id: 0, name: "first item" },
  { item_id: 1, name: "second item" },
  { item_id: 3, name: "third item" }
];

export default function App() {
  const [open, setOpen] = React.useState(false);
  const cancelItem = (item_id: number) => {
    let newItem: ItemEditModel = { itemId: item_id };
    console.log(newItem);
    setOpen(false);
    // do cancel item
  };
  const handleClickOpen = () => {
    setOpen(true);
  };
  const handleClose = () => {
    setOpen(false);
  };

  return (
    <>
      {items.map((item: IItem, index: number) => (
        <Box key={index}>
          <ListItem>
            <Typography>
              {" "}
              <b>Item {item.name}</b>
            </Typography>
            <ListItemButton onClick={handleClickOpen}>
              <ListItemText primary="Cancel" />
            </ListItemButton>
            <Dialog
              open={open}
              onClose={handleClose}
              aria-labelledby="alert-dialog-title"
              aria-describedby="alert-dialog-description"
            >
              <DialogTitle id="alert-dialog-title">Cancel</DialogTitle>
              <DialogContent>
                <DialogContentText id="alert-dialog-description">
                  Are you sure?
                </DialogContentText>
              </DialogContent>
              <DialogActions>
                <Button variant="contained" onClick={handleClose}>
                  No
                </Button>
                <Button
                  variant="contained"
                  onClick={() => cancelItem(item.item_id)}
                  autoFocus
                >
                  Yes
                </Button>
              </DialogActions>
            </Dialog>
            <ListItemButton onClick={() => console.log(item.item_id)}>
              <ListItemText primary="Edit" />
            </ListItemButton>
          </ListItem>
        </Box>
      ))}
    </>
  );
}

Here is a link to a sandbox.

1 Answers

It's not the item ID, it's the dialog. Every click of a button opens all three dialogs. You're just interacting with the last, top-most one.

There are multiple dialogs, but only one state indicating whether they are all open or closed:

const [open, setOpen] = React.useState(false);

One approach could be to use the item ID as the dialog state. So start it as undefined:

const [open, setOpen] = React.useState();

And each button can set the state to that item's ID:

<ListItemButton onClick={() => handleClickOpen(item.item_id)}>

and:

const handleClickOpen = (id) => {
  setOpen(id);
};

const handleClose = () => {
  setOpen(undefined);
};

Then check if the ID matches to determine if the dialog is open:

open={open === item.item_id}

Alternatively, with a little more refactoring you can remove the dialog component from the map entirely and have just one dialog overall. You'd then need to track the state of which record is "currently selected".

Either way, the overall goal is to only display one dialog at a time.

Related