不需要的 React 组件重新渲染?

Unwanted React Component rerender?

所以这是一个用户可以添加部分以添加问题(构建测验)的表单,我注意到当我填写 Answer Choices 并将文件放入我的 dropZone(拖放有效但未正确更新,您可以忽略此)Answer ChoicesdropZone 重新呈现,字段如刷新并变为空。

我不完全确定为什么会发生这种情况,我曾尝试查看类似的问题,但无法解决。 Here is my CodeSandbox with my App.

我在想可能是我的Questions组件中的addQuestion函数。这是代码:

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

我是 React 和 Js 的新手,所以任何 tips/explanations 都会提供很多帮助。谢谢!

当您添加新问题并更新 Questions 组件状态变量 questions(数组类型)时,整个组件(Questions 及其子组件)都会进行更新它根据新状态重新计算输出 DOM 树(虚拟 DOM)并将其与状态更改前的虚拟 DOM 进行比较的过程。在 'rerendered' 之前,它会检查两个版本的虚拟 DOMs 是否不同,如果不同,它将尽可能有效地重新渲染更改的部分。

在你的情况下,重新计算会看到许多 Question 内部的许多 Answers 组件,并且由于 Answers 没有任何道具,它基本上是对其初始的全新渲染包含 4 个空输入的状态。我能想到的最简单的解决方案是在 Questions 组件的状态下,确保 this.state.questions 数组中的每个对象都有一个 answers 属性(对象数组类型)。在 addQuestion 方法中修改 var newQuestion = { uniqueId: uuid(), question: "" }; 以将此数据包含在与该问题相关的答案中。

然后在呈现每个单独的问题时,将此答案数据(答案数组)作为道具传递给 Answers,然后根据索引(对象或对象)依次传递给每个单独的 Answer 组件一个字符串)。同时,您必须将 updateAnswers 方法作为 prop 传递给 Question,后者又传递给 AnswersAnswer,当 [=23] =]s 输入字段已更改。需要将问题id和答案的id传递给这个方法,最终修改现在应该存储在Questions组件状态中的答案数据。我调整了下面沙箱中的代码以按照这些思路工作,尽管我没有确保清除所有损坏:

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;