javascriptreactjsreact-hooksfrontend

nested component data inconsistency


I'm relatively new to react and I have a problem that I'm trying to debug for 2 days now I'm creating some sort of popup component using 'reactjs-popup' and I'm passing to its props object called items it will always contain 3 information {fullName,Phone,Relation}. Here's the Popup component class

export const PatientPopup = (props) => {
  const {
    width = '25%',
    height = '15vh',
    items = {},
    isOpenEmergencyContact = false, // Set a default value here
    setIsOpenEmergencyContact
  } = props;
  return (
    <Popup
      open={isOpenEmergencyContact}
      closeOnDocumentClick
      onClose={() => setIsOpenEmergencyContact(false)}
    >
      <Card><Scrollbar><Box
        sx={{ minWidth: 200 }}><Table>
        <TableHead>
          <TableRow>
            <TableCell>
              Name
            </TableCell>
            <TableCell>
              Mobile Number
            </TableCell>
            <TableCell>
              Relationship
            </TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          <TableRow key={items.fullName}>
            <TableCell>
              {items.fullName}
            </TableCell>
            <TableCell>
              {items.mobileNumber}
            </TableCell>
            <TableCell>
              {items.relation}
            </TableCell>
          </TableRow>


        </TableBody>
      </Table></Box></Scrollbar></Card>
    </Popup>
  );
};

PatientPopup.propTypes = {
  items: PropTypes.object,
  width: PropTypes.string,
  height: PropTypes.string
};

here is the Table component part that I call the popup component within the thing is everything seems to be working fine however the data displayed on the popup is always the last indexed data in the array. notice that this data is already coming from an array that I map on and then I pass the data to the popup component

import PropTypes from 'prop-types';
import { format } from 'date-fns';
import IdentificationIcon from '@heroicons/react/24/solid/IdentificationIcon';
import Xmark from '@heroicons/react/24/solid/XMarkIcon';
import PencilIcon from '@heroicons/react/24/solid/PencilIcon';
import {
  Avatar,
  Box,
  Card,
  Checkbox, IconButton,
  Stack, SvgIcon,
  Table,
  TableBody,
  TableCell,
  TableHead,
  TablePagination,
  TableRow,
  Typography
} from '@mui/material';
import { Scrollbar } from 'src/components/scrollbar';
import { getInitials } from 'src/utils/get-initials';
import { useState } from 'react';
import React from 'react';
import Popup from 'reactjs-popup';
import 'reactjs-popup/dist/index.css';
import { indigo } from '../../../theme/colors';
import { PatientEmergencyPopup } from './PatientEmergency-Popup';
import { PatientDeletePopup } from './Patient-DeletePopup';

export const PatientTable = (props) => {
  const {
    count = 0,
    items = [],
    onPageChange = () => {},
    onRowsPerPageChange,
    page = 0,
    rowsPerPage = 0
  } = props;

  const [activeEmergencyContactId, setActiveEmergencyContactId] = useState(null);
  const [isOpenDelete, setOpenDelete] = useState(null);
  return (
    <Card>
      <Scrollbar>
        <Box sx={{ minWidth: 800 }}>
          <Table>
            <TableHead>
              <TableRow>
                <TableCell align="center">
                  Name
                </TableCell>
                <TableCell>
                  Email
                </TableCell>
                <TableCell>
                  Gender
                </TableCell>
                <TableCell>
                  Phone
                </TableCell>
                <TableCell>
                  Username
                </TableCell>
                <TableCell>
                  Date of Birth
                </TableCell>
                <TableCell>
                  Password
                </TableCell>
                <TableCell>
                  Emergency Contact
                </TableCell>
                <TableCell>
                  Actions
                </TableCell>
              </TableRow>
            </TableHead>
            <TableBody>
              {items.map((customer) => {
                //const createdAt = format(customer.createdAt, 'dd/MM/yyyy');
                const relation = customer.emergencyContact.relation;
                const fullName = customer.emergencyContact.fullName;
                const mobileNumber = customer.emergencyContact.mobileNumber;
                console.log(customer.id);
                return (
                  <TableRow hover key={customer.id}>
                    <TableCell>
                      <Stack
                        alignItems="center"
                        direction="row"
                        spacing={2}
                      >
                        <Avatar src={customer.avatar}>
                          {getInitials(customer.name)}
                        </Avatar>
                        <Typography variant="subtitle2">
                          {customer.name}
                        </Typography>
                      </Stack>
                    </TableCell>
                    <TableCell>
                      {customer.email}
                    </TableCell>
                    <TableCell>
                      {customer.gender}
                    </TableCell>
                    <TableCell>
                      {customer.mobileNumber}
                    </TableCell>
                    <TableCell>
                      {customer.username}
                    </TableCell>
                    <TableCell>
                      {customer.dob.substring(0, customer.dob.indexOf('T'))}
                    </TableCell>
                    <TableCell>
                      {customer.password}
                    </TableCell>
                    <TableCell>
                      <IconButton
                        color="primary"
                        onClick={() => {
                          setActiveEmergencyContactId(customer.id);
                        }}
                      >
                        <SvgIcon fontSize="large">
                          <IdentificationIcon/>
                        </SvgIcon>
                      </IconButton>
                      {customer.emergencyContact.fullName}
                      <PatientEmergencyPopup
                        items={
                          {
                            fullName: fullName,
                            mobileNumber: mobileNumber,
                            relation: relation
                          }
                        }
                        width={'25%'} height={'15vh'}
                        isOpenEmergencyContact={activeEmergencyContactId === customer.id}
                        onClose={() => setActiveEmergencyContactId(null)}/>

                    </TableCell>
                    <TableCell>
                      <IconButton
                        color="primary"
                        onClick={() => {
                          setOpenDelete(true);
                        }}
                      >
                        <SvgIcon fontSize="small">
                          <Xmark/>
                        </SvgIcon>
                      </IconButton>
                      <IconButton
                        color="primary"
                        onClick={() => {
                          setOpenDelete(customer.id);
                        }}
                      >
                        <SvgIcon fontSize="small">
                          <PencilIcon/>
                        </SvgIcon>
                      </IconButton>
                      <PatientDeletePopup width={'25%'} height={'15vh'} isOpenDelete={isOpenDelete===customer.id}
                                          items={customer.name} onClose={() => setOpenDelete(null)}/>
                    </TableCell>
                  </TableRow>
                );
              })}
            </TableBody>
          </Table>
        </Box>
      </Scrollbar>
      <TablePagination
        component="div"
        count={count}
        onPageChange={onPageChange}
        onRowsPerPageChange={onRowsPerPageChange}
        page={page}
        rowsPerPage={rowsPerPage}
        rowsPerPageOptions={[5, 10, 25]}
      />
    </Card>
  );
};

PatientTable.propTypes = {
  count: PropTypes.number,
  items: PropTypes.array,
  onPageChange: PropTypes.func,
  onRowsPerPageChange: PropTypes.func,
  page: PropTypes.number,
  rowsPerPage: PropTypes.number
};

Here's the Page class that has the table component

import { useCallback, useEffect, useMemo, useState } from 'react';
import Head from 'next/head';
import { subDays, subHours } from 'date-fns';
import ArrowDownOnSquareIcon from '@heroicons/react/24/solid/ArrowDownOnSquareIcon';
import ArrowUpOnSquareIcon from '@heroicons/react/24/solid/ArrowUpOnSquareIcon';
import PlusIcon from '@heroicons/react/24/solid/PlusIcon';
import { Box, Button, Container, Stack, SvgIcon, Typography } from '@mui/material';
import { useSelection } from 'src/hooks/use-selection';
import { Layout as DashboardLayout } from 'src/layouts/dashboard/admin/layout';
import { PatientTable } from 'src/sections/admin/Patients/Patient-Table';
import { PatientsSearch } from 'src/sections/admin/Patients/patients-search';
import { applyPagination } from 'src/utils/apply-pagination';
const useCustomers = (data, page, rowsPerPage) => {
  return useMemo(
    () => {
      return applyPagination(data, page, rowsPerPage);
    },
    [data, page, rowsPerPage]
  );
};

const useCustomerIds = (customers) => {
  return useMemo(
    () => {
      return customers.map((customer) => customer.id);
    },
    [customers]
  );
};

const Page = () => {
  const [data, setData] = useState([]);
  const [page, setPage] = useState(0);
  const [rowsPerPage, setRowsPerPage] = useState(5);
  const customers = useCustomers(data, page, rowsPerPage);
  const customersIds = useCustomerIds(customers);
  const customersSelection = useSelection(customersIds);


  useEffect(() => {
    fetch('http://localhost:8000/iewPatients')
      .then((res) => {
        if (res.statusCode == 401) {
          throw new Error('Error while fetching data');
        }
        return res.json();
      })
      .then((data) => {
        setData(data['patients']);
      })
      .catch((err) => {});
  }, []);

  const handlePageChange = useCallback(
    (event, value) => {
      setPage(value);
    },
    []
  );

  const handleRowsPerPageChange = useCallback(
    (event) => {
      setRowsPerPage(event.target.value);
    },
    []
  );

  return (
    <>
      <Head>
        <title>
          Patients
        </title>
      </Head>
      <Box
        component="main"
        sx={{
          flexGrow: 1,
          py: 8
        }}
      >
        <Container maxWidth="xl">
          <Stack spacing={3}>
            <Stack
              direction="row"
              justifyContent="space-between"
              spacing={4}
            >
              <Stack spacing={1}>
                <Typography variant="h4">
                  Patients
                </Typography>
                <Stack
                  alignItems="center"
                  direction="row"
                  spacing={1}
                >
                  <Button
                    color="inherit"
                    startIcon={(
                      <SvgIcon fontSize="small">
                        <ArrowUpOnSquareIcon/>
                      </SvgIcon>
                    )}
                  >
                    Import
                  </Button>
                  <Button
                    color="inherit"
                    startIcon={(
                      <SvgIcon fontSize="small">
                        <ArrowDownOnSquareIcon/>
                      </SvgIcon>
                    )}
                  >
                    Remove
                  </Button>
                </Stack>
              </Stack>
              <div>
                <Button
                  startIcon={(
                    <SvgIcon fontSize="small">
                      <PlusIcon/>
                    </SvgIcon>
                  )}
                  variant="contained"
                >
                  Add
                </Button>
              </div>
            </Stack>
            <PatientsSearch/>
            <PatientTable
              count={data.length}
              items={customers}
              onPageChange={handlePageChange}
              onRowsPerPageChange={handleRowsPerPageChange}
              page={page}
              rowsPerPage={rowsPerPage}
            />
          </Stack>
        </Container>
      </Box>
    </>
  );
};

Page.getLayout = (page) => (
  <DashboardLayout>
    {page}
  </DashboardLayout>
);

export default Page;

is there any way to keep the data consistent among every row?


Solution

  • The problem appears to be because you have one popup instance per entry in items, but you also have a state item isOpenEmergencyContact to control visibility that can only represent "on" and "off" and not a particular customer. That means that when you click on the button that calls setIsOpenEmergencyContact(true), all of the popups open all at once on top of each other. Since they are overlapping, you see the last one, which is the one for the last item in the array.

    To fix this we need to change isOpenEmergencyContact to be an identifier of which popup should be open and not just a plain bool. Probably we should change the name of it at the same time so it's more clear what that is. We will use null for when no popup is opened and the unique customer id for when it is open.

    PatientTable

    Change

    const [isOpenEmergencyContact, setIsOpenEmergencyContact] = useState(false);
    

    To

    const [activeEmergencyContactId, setActiveEmergencyContactId] = useState(null);
    

    This is just to rename the state item to make it more clear but also to use null as the new way of saying "no popup is open".

    Change

    <IconButton
     color="primary"
     onClick={() => {
       setIsOpenEmergencyContact(true);
     }}
    >
    

    To

    <IconButton
     color="primary"
     onClick={() => {
       setActiveEmergencyContactId(customer.id);
     }}
    >
    

    When the user opens the popup, we now store the ID of the customer which that popup related to.

    Change

                 <PatientEmergencyPopup
                            items={
                              {
                                fullName: fullName,
                                mobileNumber: mobileNumber,
                                relation: relation
                              }
                            }
                            width={'25%'} height={'15vh'}
                            isOpenEmergencyContact={isOpenEmergencyContact}
                            setIsOpenEmergencyContact={setIsOpenEmergencyContact}/>
    

    To

                 <PatientEmergencyPopup
                            items={
                              {
                                fullName: fullName,
                                mobileNumber: mobileNumber,
                                relation: relation
                              }
                            }
                            width={'25%'} height={'15vh'}
                            isOpenEmergencyContact={activeEmergencyContactId === customer.id}
                            onClose={() => setActiveEmergencyContactId(null)}/>
    

    This part is the crucial bit. Notice the prop that controls if it's open is an expression that only evaluates to true for the one which has a matching customer id we stored earlier when they clicked the icon button. When the popup closes, it sets the ID of the active popup back to null.

    PatientPopup

    In order to support the change of the prop above on PatientPopup (setIsOpenEmergencyContact to onClose).

    Change

    export const PatientPopup = (props) => {
      const {
        width = '25%',
        height = '15vh',
        items = {},
        isOpenEmergencyContact = false, // Set a default value here
        setIsOpenEmergencyContact
      } = props;
      return (
        <Popup
          open={isOpenEmergencyContact}
          closeOnDocumentClick
          onClose={() => setIsOpenEmergencyContact(false)}
        >
    

    To

    export const PatientPopup = (props) => {
      const {
        width = '25%',
        height = '15vh',
        items = {},
        isOpenEmergencyContact = false, // Set a default value here
        onClose
      } = props;
      return (
        <Popup
          open={isOpenEmergencyContact}
          closeOnDocumentClick
          onClose={onClose}
        >