Search code examples
javascriptreactjsreact-bootstrapreact-forms

Why is the DOM not reflecting my component state in my dynamic form


Here's a dynamic form for student details where new student fields can be added or deleted, using the '+' & the 'X' buttons. (i.e, number of student fields is decided by user). Here, App => Parent Component & StudentsFormElement => Child Component.

Problem: Any of the 'X' buttons, when clicked, are deleting only the last student field(in DOM) and not the one which was supposed to be deleted. But most importantly, the parent component state changes correctly, with the correct student details being deleted from it. This state change is not being reflected in the DOM.

codesandbox: https://codesandbox.io/s/peaceful-spence-1j3nv?fontsize=14&hidenavigation=1&theme=dark

App component:

class App extends React.Component {
  constructor(props) {
    super(props)
    let studentsFormElementsTemp = []
    let tempSTUDENTS = {0: ["", ""]}
    this.state = {
      STUDENTS: tempSTUDENTS
    }
    studentsFormElementsTemp.push(<StudentsFormElement id="0" student={this.state.STUDENTS[0]} onStudentsChange={this.onStudentsChange} />)
    this.state = {
      studentsFormElements: studentsFormElementsTemp,
      studentsElementsIdArray: [0],
      STUDENTS: tempSTUDENTS
    } 
  }

  render() {
    return (
        <div>
            <h2 style={{textAlign: "center", display: "inline-block"}}>Students</h2><Button id="+" style={{display: "inline-block"}} variant="success" onClick={this.onStudentsChange}>+</Button>
            <form>
                {this.state.studentsFormElements}
            </form>
            <p>{JSON.stringify(this.state.STUDENTS)}</p>
        </div>
    )
  }

  onStudentsChange = (e) => {
    if (e.target.name === "studentId" || e.target.name === "studentName") { //HANDLING TYPED CHARACTERS.
      let tempSTUDENTS = this.state.STUDENTS
      if (e.target.name === "studentId") {
        tempSTUDENTS[e.target.id][0] = e.target.value
      }
      else {
        tempSTUDENTS[e.target.id][1] = e.target.value
      }
      this.setState({
        STUDENTS: tempSTUDENTS
      })
    } else { 
      let studentsFormElementsTemp = this.state.studentsFormElements
      let studentsElementsIdArrayTemp = this.state.studentsElementsIdArray
      let tempSTUDENTS = this.state.STUDENTS
      if (e.target.id === "+") { //ADDING (+) STUDENT
        tempSTUDENTS[studentsElementsIdArrayTemp[studentsElementsIdArrayTemp.length - 1] + 1] = ["", ""]
        this.setState({
          STUDENTS: tempSTUDENTS
        })
        studentsFormElementsTemp.push(<StudentsFormElement id={studentsElementsIdArrayTemp[studentsElementsIdArrayTemp.length - 1] + 1} student={this.state.STUDENTS[studentsElementsIdArrayTemp[studentsElementsIdArrayTemp.length - 1] + 1]} onStudentsChange={this.onStudentsChange} />)
        studentsElementsIdArrayTemp.push(studentsElementsIdArrayTemp[studentsElementsIdArrayTemp.length - 1] + 1)
        this.setState({
            studentsFormElements: studentsFormElementsTemp,
            studentsElementsIdArray: studentsElementsIdArrayTemp
        })
      } else { //DELETING STUDENT (X)
        let studentIndex = studentsElementsIdArrayTemp.indexOf(parseInt(e.target.id))
        studentsFormElementsTemp.splice(studentIndex, 1)
        studentsElementsIdArrayTemp.splice(studentIndex, 1)
        delete tempSTUDENTS[e.target.id]
        this.setState({
            studentsFormElements: studentsFormElementsTemp,
            studentsElementsIdArray: studentsElementsIdArrayTemp,
            STUDENTS: tempSTUDENTS
        })
      }
    }
  }
}

StudentsFormElement component:

class StudentsFormElement extends React.Component {
  render() {
    return (
      <InputGroup className="mb-3">
        <FormControl name="studentId" id={this.props.id} defaultValue={this.props.student[0]} placeholder="Id" onChange={this.props.onStudentsChange} />
        <FormControl name="studentName" id={this.props.id} defaultValue={this.props.student[1]} placeholder="Name" onChange={this.props.onStudentsChange} />
        <InputGroup.Append style={{display: "inline-block"}}>
          <Button id={this.props.id} variant="danger" onClick={this.props.onStudentsChange}>X</Button>
        </InputGroup.Append>
      </InputGroup>
    )
  }
}

Things I've tried: I've tried this.forceUpdate() just after handling 'X' button in onStudentsChange() but it doesn't make a difference.

Again, codesandbox: https://codesandbox.io/s/peaceful-spence-1j3nv?fontsize=14&hidenavigation=1&theme=dark


Solution

  • You have to add a key prop to StudentsFormElement. If you open the console you can see React throwing an error. I made two changes,

    1. Line 12:
    studentsFormElementsTemp.push(<StudentsFormElement id="0" key={0} student={this.state.STUDENTS[0]} onStudentsChange={this.onStudentsChange} />)
    
    1. Line 53:
    studentsFormElementsTemp.push(<StudentsFormElement id={studentsElementsIdArrayTemp[studentsElementsIdArrayTemp.length - 1] + 1}
              key={studentsElementsIdArrayTemp[studentsElementsIdArrayTemp.length - 1] + 1}
              student={this.state.STUDENTS[studentsElementsIdArrayTemp[studentsElementsIdArrayTemp.length - 1] + 1]} onStudentsChange={this.onStudentsChange} />)
    

    There are other refactorings I can point out, but they are irrelevant to the question asked.

    Other refactoring as you asked,

    import React from "react";
    import { InputGroup, FormControl, Button, Form } from "react-bootstrap";
    
    class App extends React.Component {
      constructor(props) {
        super(props);
        let identifier = 0;
        this.state = {
          students: [
            {
              identifier: identifier++,
              id: "",
              name: ""
            }
          ],
          identifier
        };
      }
    
      addStudent = () => {
        this.setState((prevState) => {
          const newStudents = [...prevState.students];
          newStudents.push({
            identifier: prevState.identifier,
            id: "",
            name: ""
          });
          return {
            identifier: prevState.identifier + 1,
            students: newStudents
          };
        });
      };
    
      onDeleteStudent = (identifier) => {
        console.log(identifier);
        const filteredStudents = this.state.students.filter(
          (student) => student.identifier !== identifier
        );
        this.setState({
          students: filteredStudents
        });
      };
    
      onInputChange = (event, fieldName, identifier) => {
        const newStudents = [...this.state.students];
        newStudents.forEach((student) => {
          if (student.identifier === identifier) {
            student[fieldName] = event.target.value;
          }
        });
        this.setState(newStudents);
      };
    
      render() {
        return (
          <div>
            <h2 style={{ textAlign: "center", display: "inline-block" }}>
              Students
            </h2>
            <Button
              id="+"
              style={{ display: "inline-block" }}
              variant="success"
              onClick={this.addStudent}
            >
              +
            </Button>
            <Form>
              {this.state.students.map((student, index) => (
                <StudentsFormElement
                  key={student.identifier}
                  student={student}
                  onInputChange={this.onInputChange}
                  onDeleteStudent={this.onDeleteStudent}
                />
              ))}
            </Form>
            <p>{JSON.stringify(this.state.students)}</p>
          </div>
        );
      }
    }
    
    class StudentsFormElement extends React.Component {
      render() {
        const { identifier, id, name } = this.props.student;
        return (
          <InputGroup className="mb-3">
            <FormControl
              name="id"
              defaultValue={id}
              placeholder="Id"
              onChange={(event) => {
                this.props.onInputChange(event, "id", identifier);
              }}
            />
            <FormControl
              name="name"
              defaultValue={name}
              placeholder="Name"
              onChange={(event) => {
                this.props.onInputChange(event, "name", identifier);
              }}
            />
            <InputGroup.Append style={{ display: "inline-block" }}>
              <Button
                variant="danger"
                onClick={() => {
                  this.props.onDeleteStudent(identifier);
                }}
              >
                X
              </Button>
            </InputGroup.Append>
          </InputGroup>
        );
      }
    }
    
    export default App;