javascriptreactjssetstatecodesandbox

How to remove nested object in react state without affecting loop


I am trying to dynamically add and remove text fields for user entry. When I click the button to remove a particular row, I want to modify the state object to remove the data of that row thereby causing the row to disappear. However I am unable to do that and I am getting an error in the render loop as the compiler is unable to find the value for the row. The error is as follows

Cannot read properties of undefined (reading 'from')

I want it to look at the new state object and display the number of rows accordingly. Here is the code for sandbox

import "./styles.css";
import React from "react";
import { Button, Grid, Paper } from "@mui/material";
import { TextField, Icon } from "@mui/material";

interface State {
  serialInputObjects: any;
}

class SerialQRScanClass extends React.PureComponent<State> {
  state = {
    serialInputObjects: {
      0: { from: "", to: "", except: "" }
    }
  };

  //Delete the already registered scanned codes code here

  handleAdd = () => {
    const objectLength = Object.keys(this.state.serialInputObjects).length;
    console.log(objectLength);
    this.setState((prevState) => ({
      ...prevState,
      serialInputObjects: {
        ...prevState.serialInputObjects,
        [objectLength]: {
          from: "",
          to: "",
          except: "",
          fromError: "",
          toError: ""
        }
      }
    }));
    console.log(this.state.serialInputObjects);
  };

  handleChangeFromSerials = (key: any, data: string) => {
    this.setState((prevState) => ({
      ...prevState,
      serialInputObjects: {
        ...prevState.serialInputObjects,
        [key]: { ...prevState.serialInputObjects[key], from: data }
      }
    }));
    console.log(this.state.serialInputObjects);
    //this.calculation(key);
  };

  handleChangeToSerials = (key: any, data: string) => {
    this.setState((prevState) => ({
      ...prevState,
      serialInputObjects: {
        ...prevState.serialInputObjects,
        [key]: { ...prevState.serialInputObjects[key], to: data }
      }
    }));
    console.log(this.state.serialInputObjects);
    //this.calculation(key);
  };

  handleRemove = (key) => {
    console.log(this.state.serialInputObjects);
    this.setState((prevState) => ({
      ...prevState,
      serialInputObjects: { ...prevState.serialInputObjects, [key]: undefined }
    }));
    console.log(this.state.serialInputObjects);
  };

  render() {
    return (
      <Paper elevation={3} className="abc">
        <Button onClick={this.handleAdd}>ADD NEW FIELD</Button>
        {Object.keys(this.state.serialInputObjects).map((key) => (
          <div key={key}>
            <Grid container alignItems="flex-end">
              <Grid item className="bcd">
                <TextField
                  fullWidth
                  label={"FROM"}
                  placeholder={"Ex.100"}
                  value={this.state.serialInputObjects[key]["from"]}
                  onChange={(e) =>
                    this.handleChangeFromSerials(key, e.target.value)
                  }
                  error={
                    Boolean(this.state.serialInputObjects[key]["fromError"]) ||
                    false
                  }
                  helperText={this.state.serialInputObjects[key]["fromError"]}
                  margin="none"
                  size="small"
                />
              </Grid>
              <Grid item className="bcd">
                <TextField
                  fullWidth
                  label={"To"}
                  placeholder={"Ex.100"}
                  value={this.state.serialInputObjects[key]["to"]}
                  onChange={(e) =>
                    this.handleChangeToSerials(key, e.target.value)
                  }
                  error={
                    Boolean(this.state.serialInputObjects[key]["toError"]) ||
                    false
                  }
                  helperText={this.state.serialInputObjects[key]["toError"]}
                  margin="none"
                  size="small"
                />
              </Grid>
              <Grid
                item
                className={"abc"}
                style={{ paddingLeft: "10px" }}
              ></Grid>
              <div style={{ display: key === "0" ? "none" : "block" }}>
                <Button onClick={(e) => this.handleRemove(key)}>
                  <Icon fontSize="small">remove_circle</Icon>
                </Button>
              </div>
            </Grid>
          </div>
        ))}
      </Paper>
    );
  }
}

export default function App() {
  return (
    <div className="App">
      <SerialQRScanClass />
    </div>
  );
}

Solution

  • I want to modify the state object to remove the data of that row thereby causing the row to disappear.

    If you want to cause the row to disappear you have to update the serialInputObjects object without the row you want to delete.

    Right now you are just assigning the value undefined to the selected row so it still exists but it doesn't contain the property from anymore, and because you are referring to that property here:

    value={this.state.serialInputObjects[key]["from"]}
    

    Javascript tells you that it is undefined, beacuse it doesn't exists.

    Now for this you will need destructuring assigment, this is the solution:

    handleRemoveKey(key){
      const { [key]: renamedKey, ...remainingRows } = this.state.serialInputObjects;
      this.setState((prevState) => ({
        ...prevState,
        serialInputObjects: { ...remainingRows }
      }));
    }
    

    However if you want to know why that first line of the function works, follow me.

    Let's say you have this object:

    const myObj = {
      a: '1',
      b: '2',
      c: '3',
    }
    

    And let's say we need to separate the c prop from the others, we do that like this:

    const { c, ...otherProps } = myObj
    

    This would result in the creation of 2 new const, the const c and the const otherProps:

    But what happen if there is already a variable named c? Our newly created c const in the destructuring statement would be a duplicate which is not allowed of course, so what can we do? We rename our newly created c const while we are destructuring myObj, like this:

    const { c: renamedC, ...otherProps } = obj
    

    This way the newly created const would be renamedC and therefore there will be no conflict with the other c we just supposed for the example.

    That is exactly we are doing here:

    handleRemoveKey(key){ // <------ here is already a "variable" named key
      const { [key]: renamedKey, ...remainingRows } = this.state.serialInputObjects;
      // so we had to rename our newly created 'key' const to 'renamedKey' to avoid conflicts.
    

    As a side note I would suggest serialInputObjects should be an array(of objects) and not an object because arrays are ordered while objects are not, this whole process would have been easier if serialInputObjects would have be an array.