1

I am relatively beginner in React, and in this particular situation, I am probably missing something very fundamental.

Here, I have a simple CRUD app, and after user adds new data, the updated list of items should be rendered. And the new data is added with a second Dialog component AddNewDevelopmentWork.js

So, after new data is added by the AddNewDevelopmentWork.js (which is the child compoenent that will only open a dialog for the user to input and fill few TestFields), in the main component (DevelopmentList.js), I am using componentDidUpdate to do the comparison with current state and prevState (for the state variable allDevelopmentWorks which is an array of objects) , and if they are not equal then make a request to the backend Express API and fetchin data and updating state within componentDidUpdate. And then render with new data.

Problem is, this main DevelopmentList.js component is not rendering the new data entered by the user till the page is refreshed. But after refreshing the page manually its showing the newly entered data.

Here is my DevelopmentList component.

 class DevelopmentList extends Component {
      constructor(props) {
        super(props);
        this.state = {
          allDevelopmentWorks: []
        };
      }

    componentDidUpdate(prevProps, prevState) {
        if (
          this.state.allDevelopmentWorks.length !==
          prevState.allDevelopmentWorks.length
        ) {
          return axios
            .get("/api/developmenties")
            .then(res => {
              this.setState({
                allDevelopmentWorks: res.data
              });                  
            })
            .catch(function(error) {
              console.log(error);
            });
        }
      }

      componentDidMount() {
        axios.get("/api/developmenties").then(res => {
          this.setState({
            allDevelopmentWorks: res.data
          });
        });
      }

    render() {
const { classes } = this.props;
return (
  <div>
    <Table className={classes.table}>
      <TableHead>
        <TableRow className={classes.row}>
          <CustomTableCell align="left">Location</CustomTableCell>
          <CustomTableCell align="left">
            Description Of Work
          </CustomTableCell>
          <CustomTableCell align="left">
            Date of Commencement
          </CustomTableCell>
          <CustomTableCell align="left">Date of Completion</CustomTableCell>
          <CustomTableCell align="left">Status of Work</CustomTableCell>
        </TableRow>
      </TableHead>
      <TableBody>
        {this.state.allDevelopmentWorks.map((document, i) => (
          <TableRow className={classes.row} key={i}>
            <CustomTableCell component="th" scope="row">
              {document.location}
            </CustomTableCell>
            <CustomTableCell align="left">
              {document.work_description}
            </CustomTableCell>
            <CustomTableCell align="left">
              {moment(document.date_of_commencement).format("YYYY-MM-DD")}
            </CustomTableCell>
            <CustomTableCell align="left">
              {moment(document.date_of_completion).format("YYYY-MM-DD")}
            </CustomTableCell>
            <CustomTableCell align="left">
              {document.status_of_work}
            </CustomTableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  </div>
);

  }
}
export default withStyles(styles)(DevelopmentList);

However, withing the componentDidUpdate method below, if I change it to as below the if condition (taking the length propery out of equation) then the new data is immediately rendered on the page, but then it also becomes an infinite loop inside componentDidUpdate and hitting the Express API again and again each second.

componentDidUpdate(prevProps, prevState) {
    if (
      this.state.allDevelopmentWorks !==
      prevState.allDevelopmentWorks
    ) {
      return axios
        .get("/api/developmenties")
        .then(res => {
          this.setState({
            allDevelopmentWorks: res.data
          });

    })
    .catch(function(error) {
      console.log(error);
    });
}

 }

The code in the second component, (which is the child component to the main DevelopmentList.jscomponent, that will only open a dialog for the user to input and fill few TestFields add new data to this CRUD) is below AddNewDevelopmentWork.js

class AddNewDevelopmentWork extends Component {
  state = {
    open: false,
    location: "",
    work_description: "",
    date_of_commencement: new Date(),
    date_of_completion: new Date(),
    status_of_work: "",
    vertical: "top",
    horizontal: "center"
  };

  handleCommencementDateChange = date => {
    this.setState({
      date_of_commencement: date
    });
  };

  handleCompletionDateChange = date => {
    this.setState({
      date_of_completion: date
    });
  };

  handleToggle = () => {
    this.setState({
      open: !this.state.open
    });        
  };

  handleClickOpen = () => {
    this.setState({ open: true });
  };

  handleClose = () => {
    this.props.history.push("/dashboard/developmentworks");
  };

  onChange = e => {
    const state = this.state;
    state[e.target.name] = e.target.value;
    this.setState(state);
  };

  handleFormSubmit = e => {
    e.preventDefault();
    const {
      location,
      work_description,
      date_of_commencement,
      date_of_completion,
      status_of_work
    } = this.state;
    axios
      .post("/api/developmenties/", {
        location,
        work_description,
        date_of_commencement,
        date_of_completion,
        status_of_work
      })
      .then(() => {
        // this.props.history.push("/dashboard/developmentworks");
        // window.location.href = window.location.href;
        this.setState({
          open: false,
          vertical: "top",
          horizontal: "center"
        });
      })
      .catch(error => {
        alert("Ooops something wrong happened, please try again");
      });
  };

  handleCancel = () => {
    this.setState({ open: false });
  };

  render() {
    const { classes } = this.props;
    const {
      location,
      work_description,
      date_of_commencement,
      date_of_completion,
      status_of_work,
      vertical,
      horizontal
    } = this.state;

return (
  <MuiThemeProvider theme={theme}>
    <MuiPickersUtilsProvider utils={DateFnsUtils}>
      <div>
        <MuiThemeProvider theme={theme}>
          <Dialog open={this.state.open} onClose={this.handleToggle}>
            <DialogContent required>
              <form onSubmit={this.handleFormSubmit}>
                <TextField
                  value={location}
                  onChange={e =>
                    this.setState({
                      location: e.target.value
                    })
                  }
                  error={location === ""}
                  helperText={
                    location === "" ? "Please enter Location" : " "
                  }
                  label="Location"
                  type="email"
                  fullWidth
                />
                <TextField
                  value={work_description}
                  onChange={e =>
                    this.setState({
                      work_description: e.target.value
                    })
                  }
                  error={work_description === ""}
                  helperText={
                    work_description === ""
                      ? "Please enter Work Description"
                      : " "
                  }
                  label="Description of Work"
                  type="email"
                  fullWidth
                />
                <div>
                  <DatePicker
                    format="dd/MM/yyyy"
                    label="Date of Commencement"
                    value={date_of_commencement}
                    onChange={this.handleCommencementDateChange}
                    disableOpenOnEnter
                    animateYearScrolling={false}
                  />
                </div>
                <div>
                  <DatePicker
                    format="dd/MM/yyyy"
                    label="Date of Completion"
                    value={date_of_completion}
                    onChange={this.handleCompletionDateChange}
                  />
                </div>
                <TextField
                  value={status_of_work}
                  onChange={e =>
                    this.setState({
                      status_of_work: e.target.value
                    })
                  }
                  error={location === ""}
                  helperText={
                    status_of_work === ""
                      ? "Please enter Status of Work!"
                      : " "
                  }
                  label="Status of Work"
                  type="email"
                  fullWidth
                />
              </form>
            </DialogContent>
            <DialogActions>
              <Button
                onClick={this.handleCancel}
                classes={{
                  root: classes.root
                }}
                variant="contained"
              >
                Cancel
              </Button>
              <Button
                onClick={this.handleFormSubmit}
                color="primary"
                variant="contained"
              >
                Save
              </Button>
            </DialogActions>
          </Dialog>
        </MuiThemeProvider>
      </div>
    </MuiPickersUtilsProvider>
  </MuiThemeProvider>
);
  }
}
Rohan_Paul
  • 1,504
  • 3
  • 29
  • 36
  • Possible duplicate of [setState() inside of componentDidUpdate()](https://stackoverflow.com/questions/30528348/setstate-inside-of-componentdidupdate) – Ian Kemp Jan 07 '19 at 18:02

4 Answers4

2

The problem is that you are not treating your state as if it were immutable (as recommended by the React documentation). When you call this.setState({ allDevelopmentWorks: res.data }), you are replacing the value of allDevelopmentWorks with a new array with a new object reference. Because the array references do not match, checking for equality directly will fail (i.e. this.state.allDevelopmentWorks !== prevState.allDevelopmentWorks is comparing object references).

Checkout this answer for updating a state array without mutating it.

And take a look at loadash isEqual for comparing array equality.

Don Brody
  • 1,689
  • 2
  • 18
  • 30
1

The resulting infinite loop occurring has little to do with React itself but rather how javascript handles comparison between two objects. Since data type returned from the API is an array

[] !== [] => // true

The condition will always be true and hence setState is called repeatedly for every state change and this will trigger a re-render. A better understanding of how the different component lifecycle methods are invoked is one of the core concepts that I had to get a hang of when in my early days of learning React.

componentWillMount ->
render ->
componentDidMount(state changes here will trigger a re-render from maybe an api fetch) -> 
componentWillUpdate ->
render ->
componentDidUpdate

You could share a link to the repo if it there is one and I can have a look

Kayode Adeola
  • 45
  • 1
  • 8
0

If you use setState inside componentDidUpdate it updates the component, resulting in a call to componentDidUpdate which subsequently calls setState again resulting in the infinite loop. You should conditionally call setState and ensure that the condition violating the call occurs eventually e.g:

componentDidUpdate: function() {
    if (condition) {
        this.setState({..})
    } else {
        //do something else
    }
}

In case you are only updating the component by sending props to it(it is not being updated by setState, except for the case inside componentDidUpdate), you can call setState inside componentWillReceiveProps instead of componentDidUpdate.

Kamal
  • 2,550
  • 1
  • 8
  • 13
0

Answering my own question, after I resolved the issue. It was a problem of not updating the parent component's (DevelopmentList.js) state at all when the user was adding a new item with the child component.(AddNewDevelopmentWork.js which is a form Dialog). So, it was case of passing data from child to parent to update parent's state as below

  • A> Define a callback in my parent (addItem function) which takes the data I need in as a parameter.

  • B> Pass that callback as a prop to the child

  • C> Call the callback using this.props.[callback] inside the child, and pass in the data as the argument.

Here's my final working code in the parent DevelopmentList.js

 class DevelopmentList extends Component {
      constructor(props) {
        super(props);
        this.state = {
          allDevelopmentWorks: []
        };
      }

      addItem = item => {
        this.setState({
          allDevelopmentWorks: [item, ...this.state.allDevelopmentWorks]
        });
      };

      componentDidMount() {
        axios.get("/api/developmenties").then(res => {
          this.setState({
            allDevelopmentWorks: res.data
          });
        });
      }

     componentDidUpdate(prevProps, prevState) {
        if (
         this.state.allDevelopmentWorks.length !==
         prevState.allDevelopmentWorks.length
         ) {
          return axios
            .get("/api/developmenties")
            .then(res => {
              this.setState({
                allDevelopmentWorks: res.data
            });
          })
         .catch(function(error) {
           console.log(error);
         });
    }
  }

      deleteDevelopmentWorks = id => {
        axios.delete("/api/developmenties/" + id).then(() => {
          this.setState({
            allDevelopmentWorks: this.state.allDevelopmentWorks.filter(
              item => item._id !== id
            )
          });
        });
      };

      render() {
        const { classes } = this.props;
        return (
          <div>
            <AddNewDevelopmentWork addNewItemToParentState={this.addItem} />

            <Table className={classes.table}>
              <TableBody>
                {this.state.allDevelopmentWorks.map((document, i) => (
                  <TableRow className={classes.row} key={i}>
                    <CustomTableCell component="th" scope="row">
                      {document.location}
                    </CustomTableCell>
                    <CustomTableCell align="left">
                      {document.work_description}
                    </CustomTableCell>
                    <CustomTableCell align="left">
                      {moment(document.date_of_commencement).format("YYYY-MM-DD")}
                    </CustomTableCell>
                    <CustomTableCell align="left">
                      {moment(document.date_of_completion).format("YYYY-MM-DD")}
                    </CustomTableCell>
                    <CustomTableCell align="left">
                      {document.status_of_work}
                    </CustomTableCell>

                    <CustomTableCell align="left">
                      <div id="snackbar">
                        The Document has been successfully deleted
                      </div>
                      <Button
                        onClick={this.deleteDevelopmentWorks.bind(
                          this,
                          document._id
                        )}
                        variant="contained"
                        className={classes.button}
                      >
                        <DeleteIcon className={classes.rightIcon} />
                      </Button>
                    </CustomTableCell>
                  </TableRow>
                ))}
              </TableBody>
            </Table>
          </div>
        );
      }
    }

    export default withStyles(styles)(DevelopmentList);

And here's my final working code in the child AddNewDevelopmentWork.js

class AddNewDevelopmentWork extends Component {
  state = {
    open: false,
    opensnackbar: false,
    vertical: "top",
    horizontal: "center",
    location: "",
    work_description: "",
    date_of_commencement: new Date(),
    date_of_completion: new Date(),
    status_of_work: ""
  };

  handleCommencementDateChange = date => {
    this.setState({
      date_of_commencement: date
    });
  };

  handleCompletionDateChange = date => {
    this.setState({
      date_of_completion: date
    });
  };

  handleToggle = () => {
    this.setState({
      open: !this.state.open
    });
  };

  handleClickOpen = () => {
    this.setState({ open: true });
  };

  handleClose = () => {
    this.setState({ opensnackbar: false });
    this.props.history.push("/dashboard/developmentworks");
  };

  onChange = e => {
    const state = this.state;
    state[e.target.name] = e.target.value;
    this.setState(state);
  };

  handleFormSubmit = e => {
    e.preventDefault();
    const { addNewItemToParentState } = this.props;
    const {
      location,
      work_description,
      date_of_commencement,
      date_of_completion,
      status_of_work
    } = this.state;
    axios
      .post("/api/developmenties/", {
        location,
        work_description,
        date_of_commencement,
        date_of_completion,
        status_of_work
      })
      .then(() => {
        addNewItemToParentState({
          location,
          work_description,
          date_of_commencement,
          date_of_completion,
          status_of_work
        });
        this.setState({
          open: false,
          opensnackbar: true,
          vertical: "top",
          horizontal: "center"
        });
      })
      .catch(error => {
        alert("Ooops something wrong happened, please try again");
      });
  };

  handleCancel = () => {
    this.setState({ open: false });
  };

  render() {
    const { classes } = this.props;
    const {
      location,
      work_description,
      date_of_commencement,
      date_of_completion,
      status_of_work,
      vertical,
      horizontal,
      opensnackbar
    } = this.state;

    return (
      <MuiThemeProvider theme={theme}>
        <MuiPickersUtilsProvider utils={DateFnsUtils}>
          <div>
            <MuiThemeProvider theme={theme}>
              <Fab
                variant="fab"
                onClick={this.handleClickOpen}
                aria-pressed="true"
                color="secondary"
                size="large"
                aria-label="Add"
                fontSize="large"
              >
                <AddIcon className={styles.largeIcon} />
              </Fab>
              <Dialog
                open={this.state.open}
                onClose={this.handleToggle}
                aria-labelledby="form-dialog-title"
                fullWidth={true}
                maxWidth={"md"}
              >
                <DialogTitle
                  id="form-dialog-title"
                  disableTypography="false"
                  className={this.props.classes.styledHeader}
                >
                  New Development Work
                </DialogTitle>
                <DialogContent required>
                  <form onSubmit={this.handleFormSubmit}>
                    <TextField
                      value={location}
                      onChange={e =>
                        this.setState({
                          location: e.target.value
                        })
                      }
                      type="email"
                    />
                    <TextField
                      value={work_description}
                      onChange={e =>
                        this.setState({
                          work_description: e.target.value
                        })
                      }
                      type="email"
                    />
                    <div>
                      <DatePicker
                        value={date_of_commencement}
                        onChange={this.handleCommencementDateChange}
                      />
                    </div>
                    <div>
                      <DatePicker
                        value={date_of_completion}
                        onChange={this.handleCompletionDateChange}
                      />
                    </div>
                    <TextField
                      value={status_of_work}
                      onChange={e =>
                        this.setState({
                          status_of_work: e.target.value
                        })
                      }
                      type="email"
                      fullWidth
                    />
                  </form>
                </DialogContent>
                <DialogActions>
                  <Button
                    onClick={this.handleCancel}
                    classes={{
                      root: classes.root
                    }}
                    variant="contained"
                  >
                    Cancel
                  </Button>
                  <Button
                    onClick={this.handleFormSubmit}
                    color="primary"
                    variant="contained"
                  >
                    Save
                  </Button>
                </DialogActions>
              </Dialog>
              <Snackbar
                anchorOrigin={{ vertical, horizontal }}
                open={opensnackbar}
                autoHideDuration={2000}
                onClose={this.handleClose}
              >
                <MySnackbarContent
                  onClose={this.handleClose}
                  variant="success"
                  message="New Development Works has been uploaded successfully"
                />
              </Snackbar>
            </MuiThemeProvider>
          </div>
        </MuiPickersUtilsProvider>
      </MuiThemeProvider>
    );
  }
}

AddNewDevelopmentWork.propTypes = {
  classes: PropTypes.object.isRequired
};

export default withStyles(styles)(AddNewDevelopmentWork);
Rohan_Paul
  • 1,504
  • 3
  • 29
  • 36