reactjsreact-hooksmaterial-uimui-x-data-grid

React usestate not update value object


I am working with a variable in useState and when I try to update a property of this object the change is not set.

This is the function:

const addItemVoucher = () => {
  const totalItem = valueItem.quantity * valueItem.price;
  setValueItem((state) => ({ ...state, total: totalItem }));
  console.log(valueItem);
  const itemTemp = Object.assign({}, valueItem);
  setItemVouchers((itemVouchers) => [...itemVouchers, itemTemp]);
  setValueItem({
    id: "",
    article: "",
    price: 0.0,
    quantity: 0.0,
    total: 0.0
  });
};

I assign the value of "totalitem" to the "total" property of the variable "valueItem", but it does not work, so when I print on the screen the value of total, in a datagrid, is 0

code:

import React, { useState, Fragment } from "react";
import Grid from "@mui/material/Grid";
import TextField from "@mui/material/TextField";
import Container from "@mui/material/Container";
import Box from "@mui/material/Box";
import { DataGrid } from "@mui/x-data-grid";
import { Button, gridClasses } from "@mui/material";

const TmpItem = () => {
   const [pageSize, setPageSize] = useState(15);
   const [valueItem, setValueItem] = useState({
     id: "",
     article: "",
     price: 0.0,
     quantity: 0.0,
     total: 0.0
   });
   const [itemVouchers, setItemVouchers] = useState([]);

   const columnsItem = [
    {
  field: "index",
  width: 70,
  renderHeader: () => <strong>{"NRO"}</strong>,
  headerAlign: "center",
  renderCell: (index) => index.api.getRowIndex(index.row.id) + 1
},
{
  field: "article",
  width: 300,
  renderHeader: () => <strong>{"NAME"}</strong>,
  headerAlign: "center"
},
{
  field: "quantity",
  width: 100,
  renderHeader: () => <strong>{"QUANTITY"}</strong>,
  headerAlign: "center"
},
{
  field: "price",
  width: 100,
  renderHeader: () => <strong>{"PRICE"}</strong>,
  type: "number",
  cellClassName: "font-tabular-nums",
  headerAlign: "center"
},
{
  field: "total",
  width: 100,
  renderHeader: () => <strong>{"TOTAL"}</strong>,
  type: "number",
  cellClassName: "font-tabular-nums",
  headerAlign: "center"
}
];

const handleChangeValueItem = (event) => {
  setValueItem({ ...valueItem, [event.target.name]: event.target.value });
};

const addItemVoucher = () => {
  const totalItem = valueItem.quantity * valueItem.price;
  setValueItem((state) => ({ ...state, total: totalItem }));
  console.log(valueItem);
  const itemTemp = Object.assign({}, valueItem);
  setItemVouchers((itemVouchers) => [...itemVouchers, itemTemp]);
  setValueItem({
    id: "",
    article: "",
    price: 0.0,
    quantity: 0.0,
    total: 0.0
  });
 };

 return (
  <Fragment>
  <Container maxWidth="md">
    <Box sx={{ flexGrow: 1 }}>
      <form id="new-form-voucher">
        <Grid
          container
          spacing={{ xs: 1, md: 2 }}
          columns={{ xs: 4, sm: 8, md: 12 }}
        >
          <Grid item xs={4} sm={4} md={6}>
            <TextField
              label="ID"
              name="id"
              value={valueItem.id}
              variant="outlined"
              fullWidth
              onChange={handleChangeValueItem}
              size="small"
            />
          </Grid>
          <Grid item xs={4} sm={4} md={6}>
            <TextField
              label="Article"
              name="article"
              value={valueItem.article}
              variant="outlined"
              fullWidth
              onChange={handleChangeValueItem}
              size="small"
            />
          </Grid>
          <Grid item xs={4} sm={4} md={6}>
            <TextField
              label="Price"
              name="price"
              value={valueItem.price}
              type="number"
              variant="outlined"
              fullWidth
              onChange={handleChangeValueItem}
              size="small"
            />
          </Grid>
          <Grid item xs={4} sm={4} md={6}>
            <TextField
              label="Quantity"
              name="quantity"
              value={valueItem.quantity}
              type="number"
              variant="outlined"
              fullWidth
              onChange={handleChangeValueItem}
              size="small"
            />
          </Grid>
          <Grid item xs={4} sm={4} md={6}>
            <Button fullWidth onClick={addItemVoucher} variant="contained">
              Add
            </Button>
          </Grid>
          <Grid item xs={4} sm={8} md={12} textAlign="center">
            <div style={{ width: "100%" }}>
              <DataGrid
                autoHeight
                disableExtendRowFullWidth
                disableColumnFilter
                disableSelectionOnClick
                showCellRightBorder
                showColumnRightBorder
                rows={itemVouchers}
                columns={columnsItem}
                pageSize={pageSize}
                onPageSizeChange={(newPageSize) => setPageSize(newPageSize)}
                rowsPerPageOptions={[15, 20, 30]}
                getRowHeight={() => "auto"}
                sx={{
                  [`& .${gridClasses.cell}`]: { py: 1 }
                }}
                pagination
              />
            </div>
          </Grid>
        </Grid>
      </form>
    </Box>
  </Container>
  </Fragment>
  );
 };

 export default TmpItem;

code in sandbox : https://codesandbox.io/s/reverent-tree-rnc31d?file=/TmpItem.js:0-5075

Thanks for all.


Solution

  • Let me explain the problem in the above code by adding comments:

    const addItemVoucher = () => {
      const totalItem = valueItem.quantity * valueItem.price;
      // this will tell React to chain an update event on the "valueItem"
      // but this will happen on the next re-render of your component
      setValueItem((state) => ({ ...state, total: totalItem }));
      // so this will still log the current valueItem with the old "total"
      console.log(valueItem);
      // now you are creating here an outdated item copy
      const itemTemp = Object.assign({}, valueItem);
      // you add this outdated copy to your list, but for next re-render
      setItemVouchers((itemVouchers) => [...itemVouchers, itemTemp]);
      // you update the "valueItem" again, but again for next re-render
      // since React@18 this means you only see this update and NOT the
      // previous update you did before
      setValueItem({
        id: "",
        article: "",
        price: 0.0,
        quantity: 0.0,
        total: 0.0
      });
     };
    

    To fix this, you might want to do this instead:

    const addItemVoucher = () => {
      const totalItem = valueItem.quantity * valueItem.price;
      setItemVouchers((itemVouchers) => [...itemVouchers, { ...valueItem, total: totalItem }]);
      setValueItem({
        id: "",
        article: "",
        price: 0.0,
        quantity: 0.0,
        total: 0.0
      });
     };