javascriptreactjsstates

Unwanted React Component rerender?


So this is a form where the user can add sections to add a question (to build a quiz) and I notice that when I fill in the Answer Choices and put a file in my dropZone (drop works but doesn't update correctly, you can ignore this) the Answer Choices and dropZone rerender and the fields like refresh and become empty.

I am not entirely sure why this is happening, i have tried looking at similar issues but I couldn't get it to work. Here is my CodeSandbox with my App.

I am thinking that it may be the addQuestion function in my Questions component. Here's the code for that:

  addQuestion = question => {
    questionIdx++;
    var newQuestion = { uniqueId: uuid(), question: "" }
    this.setState(prevState => ({
      questions: [...prevState.questions, newQuestion]
    }));
    return { questions: newQuestion }
  };

I am new to React and Js so any tips/explanations will do loads of help. Thanks!


Solution

  • When you add a new question, and update the Questions components state variable questions (Array type), the whole component (Questions and its children) goes through an update process where it recalculates the output DOM tree (a virtual DOM) based on the new state and compares it to the pre-state-change virtual DOM. Before anything is 'rerendered' it checks to see if the virtual DOMs of the two versions are different among other things, if they are it will rerender the parts that change as efficiently as possible.

    In your case, the recalculation sees many Answers component inside of many Question and since Answers doesn't have any props, it is basically a fresh render to its initial state which includes 4 empty inputs. The simplest solution I can think of is in the state of the Questions component, make sure each object in the this.state.questions Array has an answers attribute (of type Array of Objects). In the addQuestion method modify var newQuestion = { uniqueId: uuid(), question: "" }; to includes this data on the answers related to that question.

    Then when rendering each individual question, pass this answer data (Array of answers) as a prop to Answers and in turn to each individual Answer component based on the index (an Object or a String). At the same time, you must pass an updateAnswers method as a prop to Question that in turn is passed to Answers and Answer, that is called when an Answers input field is changed. The question id and the id of the answer needs to be passed to this method to ultimately modify the answer data that should be now stored in the state of the Questions component. I adjusted the code from the sandbox below to work along these lines, although I didn't make sure to cleanup all the damage:

    import React, { Component } from "react";
    import "./App.css";
    
    var uuid = require("uuid-v4");
    // Generate a new UUID
    var myUUID = uuid();
    // Validate a UUID as proper V4 format
    uuid.isUUID(myUUID); // true
    
    class DropZone extends Component {
      constructor(props) {
        super(props);
        this.state = {
          file: "",
          fileId: uuid(),
          className: "dropZone"
        };
        this.handleChange = this.handleChange.bind(this);
        this._onDragEnter = this._onDragEnter.bind(this);
        this._onDragLeave = this._onDragLeave.bind(this);
        this._onDragOver = this._onDragOver.bind(this);
        this._onDrop = this._onDrop.bind(this);
      }
    
      handleChange(file = "") {
        this.setState({
          file: URL.createObjectURL(file)
        });
        //document.getElementsByClassName("dropZone").style.backgroundImage = 'url(' + this.state.file + ')';
      }
    
      componentDidMount() {
        window.addEventListener("mouseup", this._onDragLeave);
        window.addEventListener("dragenter", this._onDragEnter);
        window.addEventListener("dragover", this._onDragOver);
        document
          .getElementById("dragbox")
          .addEventListener("dragleave", this._onDragLeave);
        window.addEventListener("drop", this._onDrop);
      }
    
      componentWillUnmount() {
        window.removeEventListener("mouseup", this._onDragLeave);
        window.removeEventListener("dragenter", this._onDragEnter);
        window.addEventListener("dragover", this._onDragOver);
        document
          .getElementById("dragbox")
          .removeEventListener("dragleave", this._onDragLeave);
        window.removeEventListener("drop", this._onDrop);
      }
    
      _onDragEnter(e) {
        e.stopPropagation();
        e.preventDefault();
        return false;
      }
    
      _onDragOver(e) {
        e.preventDefault();
        e.stopPropagation();
        return false;
      }
    
      _onDragLeave(e) {
        e.stopPropagation();
        e.preventDefault();
        return false;
      }
    
      _onDrop(e, event) {
        e.preventDefault();
        this.handleChange(e.dataTransfer.files[0]);
        let files = e.dataTransfer.files;
        console.log("Files dropped: ", files);
        // Upload files
        console.log(this.state.file);
        return false;
      }
    
      render() {
        const uniqueId = this.state.fileId;
        return (
          <div>
            <input
              type="file"
              id={uniqueId}
              name={uniqueId}
              class="inputFile"
              onChange={e => this.handleChange(e.target.files[0])}
            />
            <label htmlFor={uniqueId} value={this.state.file}>
              {this.props.children}
              <div className="dropZone" id="dragbox" onChange={this.handleChange}>
                Drop or Choose File
                <img src={this.state.file} id="pic" name="file" accept="image/*" />
              </div>
            </label>
            <div />
          </div>
        );
      }
    }
    
    class Answers extends Component {
      constructor(props) {
        super(props);
        this.state = {
          answers: props.answers,
        };
        this.handleUpdate = this.handleUpdate.bind(this);
      }
    
      // let event = {
      //   index: 1,
      //   value: 'hello'
      // };
      handleUpdate(event) {
        //if ("1" == 1) // true
        //if ("1" === 1) //false
        // var answers = this.state.answers;
        // answers[event.index] = event.value;
        // this.setState(() => ({
        //   answers: answers
        // }));
    
        var answers = this.state.answers.slice();
    
        for (var i = 0; i < answers.length; i++) {
          if (answers[i].answerId == event.answerId) {
            answers[i].answer = event.value;
            break;
          }
        }
        this.setState(() => ({
          answers: answers
        }));
        this.props.updateAnswers(answers)
    
        console.log(event);
      }
    
      render() {
        return (
          <div id="answers">
            Answer Choices<br />
            {this.state.answers.map((value, index) => (
              <Answer
                key={`${value}-${index}`}
                onUpdate={this.handleUpdate}
                value={value}
                number={index}
                name="answer"
              />
            ))}
          </div>
        );
      }
    }
    
    class Answer extends Component {
      constructor(props) {
        super(props);
        this.state = {
          answer: props.value.answer,
          answerId: props.value.answerId,
          isCorrect: props.value.isCorrect,
        };
        this.handleChange = this.handleChange.bind(this);
      }
    
      handleChange(event) {
        const target = event.target;
        const value = target.type === "checkbox" ? target.checked : target.value;
        this.setState({
          answer: value
        });
        this.props.onUpdate({
          answerId: this.state.answerId,
          value
        });
    
        // let sample = {
        //   kyle: "toast",
        //   cam: "pine"
        // };
    
        // sample.kyle
        // sample.cam
      }
      render() {
        return (
          <div>
            <input type="checkbox" />
            <input
              type="text"
              value={this.state.answer}
              onChange={this.handleChange}
              key={this.state.answerId}
              name="answer"
            />
            {/*console.log(this.state.answerId)*/}
          </div>
        );
      }
    }
    
    var questionIdx = 0;
    
    class Questions extends Component {
      constructor(props) {
        super(props);
        this.state = {
          questions: []
        };
        this.handleUpdate = this.handleUpdate.bind(this);
        this.handleUpdate = this.handleUpdate.bind(this);
        this.removeQuestion = this.removeQuestion.bind(this);
      }
    
      handleUpdate(event) {
        //if ("1" == 1) // true
        //if ("1" === 1) //false
        var questions = this.state.questions.slice();
    
        for (var i = 0; i < questions.length; i++) {
          if (questions[i].uniqueId == event.uniqueId) {
            questions[i].question = event.value;
            break;
          }
        }
        this.setState(() => ({
          questions: questions
        }));
    
        console.log(event, questions);
      }
    
      updateAnswers(answers, uniqueId) {
        const questions = this.state.questions
        questions.forEach((question) => {
          if (question.uniqueId === uniqueId) {
            question.answers = answers
          }
        })
        this.setState({
          questions,
        })
      }
    
      addQuestion = question => {
        questionIdx++;
        var newQuestion = { 
          uniqueId: uuid(),
          question: "",
          answers: [
            { answer: "", answerId: uuid(), isCorrect: false,},
            { answer: "", answerId: uuid(), isCorrect: false,},
            { answer: "", answerId: uuid(), isCorrect: false,},
            { answer: "", answerId: uuid(), isCorrect: false,}]
          }
        this.setState(prevState => ({
          questions: [...prevState.questions, newQuestion]
        }));
        return { questions: newQuestion };
      };
    
      removeQuestion(uniqueId, questions) {
        this.setState(({ questions }) => {
          var questionRemoved = this.state.questions.filter(
            props => props.uniqueId !== uniqueId
          );
          return { questions: questionRemoved };
        });
        console.log(
          "remove button",
          uniqueId,
          JSON.stringify(this.state.questions, null, " ")
        );
      }
    
      render() {
        return (
          <div id="questions">
            <ol id="quesitonsList">
              {this.state.questions.map((value, index) => (
                <li key={value.uniqueId}>
                  {
                    <RemoveQuestionButton
                      onClick={this.removeQuestion}
                      value={value.uniqueId}
                    />
                  }
                  {
                    <Question
                      onUpdate={this.handleUpdate}
                      value={value}
                      number={index}
                      updateAnswers={(answers) => this.updateAnswers(answers, value.uniqueId) }
                    />
                  }
                  {<br />}
                </li>
              ))}
            </ol>
            <AddQuestionButton onClick={this.addQuestion} />
          </div>
        );
      }
    }
    
    class Question extends Component {
      constructor(props) {
        super(props);
        this.state = {
          question: props.value.question,
          uniqueId: props.value.uniqueId,
          answers: props.value.answers,
        };
        this.handleChange = this.handleChange.bind(this);
      }
      handleChange(event) {
        const target = event.target;
        const value = target.type === "checkbox" ? target.checked : target.value;
        this.setState({
          question: value
        });
        this.props.onUpdate({
          uniqueId: this.state.uniqueId,
          value
        });
      }
    
      render() {
        return (
          <div id={"questionDiv" + questionIdx} key={myUUID + questionIdx + 1}>
            Question<br />
            <input
              type="text"
              value={this.state.question}
              onChange={this.handleChange}
              key={this.state.uniqueId}
              name="question"
            />
            <DropZone />
            <Answers updateAnswers={this.props.updateAnswers} answers={this.state.answers} />
          </div>
        );
      }
    }
    
    class IntroFields extends Component {
      constructor(props) {
        super(props);
        this.state = {
          title: "",
          author: ""
        };
        this.handleChange = this.handleChange.bind(this);
      }
    
      handleChange(event) {
        const target = event.target;
        const value = target.type === "checkbox" ? target.checked : target.value;
        const name = target.name;
        console.log([name]);
        this.setState((previousState, props) => ({
          [name]: value
        }));
      }
    
      render() {
        return (
          <div id="IntroFields">
            Title:{" "}
            <input
              type="text"
              value={this.state.title}
              onChange={this.handleChange}
              name="title"
            />
            Author:{" "}
            <input
              type="text"
              value={this.state.author}
              onChange={this.handleChange}
              name="author"
            />
          </div>
        );
      }
    }
    
    class AddQuestionButton extends Component {
      addQuestion = () => {
        this.props.onClick();
      };
    
      render() {
        return (
          <div id="addQuestionButtonDiv">
            <button id="button" onClick={this.addQuestion} />
            <label id="addQuestionButton" onClick={this.addQuestion}>
              Add Question
            </label>
          </div>
        );
      }
    }
    
    class RemoveQuestionButton extends Component {
      removeQuestion = () => {
        this.props.onClick(this.props.value);
      };
    
      render() {
        return (
          <div id="removeQuestionButtonDiv">
            <button id="button" onClick={this.removeQuestion} key={uuid()} />
            <label
              id="removeQuestionButton"
              onClick={this.removeQuestion}
              key={uuid()}
            >
              Remove Question
            </label>
          </div>
        );
      }
    }
    
    class BuilderForm extends Component {
      render() {
        return (
          <div id="formDiv">
            <IntroFields />
            <Questions />
          </div>
        );
      }
    }
    export default BuilderForm;