dorkycam
dorkycam

Reputation: 529

Can't seem to keep my DropZone state(s) updated/always rerenders React?

So my app is a form that has dropZones (amongst other things) and an Add Questions button that adds another dropZone to the form. Whenever I put an image in my dropZone and then click Add Question the image disappears. Here's a CodeSandbox of the whole app.

But if you prefer relevant code only, here's my DropZone component followed by my AddQuestionButton component:

class DropZone extends Component {

  constructor(props) {
    super(props);

    this.dropZoneRef = React.createRef();

    this.state = {
      fileBlob: props.fileBlob,
      fileId: props.fileId
    };
    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({
      fileBlob: URL.createObjectURL(file)
    });
    console.log(this.state.fileBlob + "OMG")
    //document.getElementsByClassName("dropZone").style.backgroundImage = 'url(' + this.state.file + ')';
  }

  handleUpdate(){

  }

  componentDidMount(event) {
    this.dropZoneRef.current.addEventListener("mouseup", this._onDragLeave);
    this.dropZoneRef.current.addEventListener("dragenter", this._onDragEnter);
    this.dropZoneRef.current.addEventListener("dragover", this._onDragOver);
    this.dropZoneRef.current.addEventListener("dragleave", this._onDragLeave);
    this.dropZoneRef.current.removeEventListener("drop", this._onDrop);
    window.addEventListener("dragover",function(e){
      e = e || event;
      e.preventDefault();
    },false);
    window.addEventListener("drop",function(e){
      e = e || event;
      e.preventDefault();
    },false);
  }

  componentWillUnmount() {
    this.dropZoneRef.current.removeEventListener("mouseup", this._onDragLeave);
    this.dropZoneRef.current.removeEventListener("dragenter", this._onDragEnter);
    this.dropZoneRef.current.addEventListener("dragover", this._onDragOver);
    this.dropZoneRef.current.removeEventListener("dragleave", this._onDragLeave);
    this.dropZoneRef.current.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.fileBlob);
        return false;
      }

      render() {
        const labelId = uuid();
        return (
          <div>
            <input
              type="file"
              id={labelId}
              name={this.state.fileBlobId}
              className="inputFile"

              onChange={e => this.handleChange(e.target.files[0])}
            />
            <label htmlFor={labelId} value={this.state.fileBlob}>
              {this.props.children}
              <div className="dropZone" id="dragbox" key={this.state.fileBlobId} ref={this.dropZoneRef} onChange={this.handleChange} onDrop={this._onDrop}>
                Drop or Choose File {console.log(this.dropZoneRef)}
                <img src={this.state.fileBlob} id="pic" name="file" accept="image/*" />
              </div>
            </label>
            <div />
          </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>
    );
  }
}

And here's the direct parent of the DropZone component, Question:

class Question extends Component {
  constructor(props) {
    super(props);
    this.state = {
      question: props.value.question,
      uniqueId: props.value.uniqueId,
      answers: props.value.answers,
      file: props.file
    };
    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
    });
  }

  handleUpdate(event, file) {
    //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].file = event.value;
        break;
      }
    }
    this.setState(() => ({
      questions: questions
    }));

    console.log(event, questions);
  }

  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 file={this.state.file}/>
        <Answers
          updateAnswers={this.props.updateAnswers}
          answers={this.state.answers}
        />
      </div>
    );
  }
}

And Question's parent component, `Questions':

class Questions extends Component {
  constructor(props) {
    super(props);
    this.state = {
      questions: []
    };
    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: "",
      file: { fileBlob: "", fileId: uuid()},
      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>
    );
  }
}

Thanks!

Upvotes: 0

Views: 965

Answers (1)

lrcrb
lrcrb

Reputation: 910

Only keep state in the top level component, and you should be good to go.

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.dropZoneRef = React.createRef();
    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.props.updateFile(URL.createObjectURL(file), this.props.file.fileId);
    //document.getElementsByClassName("dropZone").style.backgroundImage = 'url(' + this.state.file + ')';
  }

  componentDidMount(event) {
    this.dropZoneRef.current.addEventListener("mouseup", this._onDragLeave);
    this.dropZoneRef.current.addEventListener("dragenter", this._onDragEnter);
    this.dropZoneRef.current.addEventListener("dragover", this._onDragOver);
    this.dropZoneRef.current.addEventListener("dragleave", this._onDragLeave);
    this.dropZoneRef.current.removeEventListener("drop", this._onDrop);
    window.addEventListener(
      "dragover",
      function(e) {
        e = e || event;
        e.preventDefault();
      },
      false
    );
    window.addEventListener(
      "drop",
      function(e) {
        e = e || event;
        e.preventDefault();
      },
      false
    );
  }

  componentWillUnmount() {
    this.dropZoneRef.current.removeEventListener("mouseup", this._onDragLeave);
    this.dropZoneRef.current.removeEventListener(
      "dragenter",
      this._onDragEnter
    );
    this.dropZoneRef.current.addEventListener("dragover", this._onDragOver);
    this.dropZoneRef.current.removeEventListener(
      "dragleave",
      this._onDragLeave
    );
    this.dropZoneRef.current.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
    return false;
  }

  render() {
    const labelId = uuid();
    return (
      <div>
        <input
          type="file"
          id={labelId}
          name={this.props.file.fileId}
          className="inputFile"
          onChange={e => this.handleChange(e.target.files[0])}
        />
        <label htmlFor={labelId} value={this.props.file.fileBlob}>
          {this.props.children}
          <div
            className="dropZone"
            id="dragbox"
            key={this.props.file.fileId}
            ref={this.dropZoneRef}
            onChange={this.handleChange}
            onDrop={this._onDrop}
          >
            Drop or Choose File {console.log(this.dropZoneRef)}
            <img
              src={this.props.file.fileBlob}
              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) {
    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.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
    });
  }

  updateFile(fileBlob, fileId) {
    const questions = this.state.questions;
    questions.forEach(question => {
      if (question.file.fileId === fileId) {
        question.file.fileBlob = fileBlob;
      }
    });
    this.setState({
      questions
    });
  }

  addQuestion = question => {
    questionIdx++;
    var newQuestion = {
      uniqueId: uuid(),
      question: "",
      file: { fileBlob: {}, fileId: uuid() },
      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)
                  }
                  updateFile={this.updateFile.bind(this)}
                />
              }
              {<br />}
            </li>
          ))}
        </ol>
        <AddQuestionButton onClick={this.addQuestion} />
      </div>
    );
  }
}

class Question extends Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }
  handleChange(event) {
    const target = event.target;
    const value = target.type === "checkbox" ? target.checked : target.value;
    this.props.onUpdate({
      uniqueId: this.props.value.uniqueId,
      value
    });
  }

  render() {
    return (
      <div id={"questionDiv" + questionIdx} key={myUUID + questionIdx + 1}>
        Question<br />
        <input
          type="text"
          value={this.props.value.question}
          onChange={this.handleChange}
          key={this.props.value.uniqueId}
          name="question"
        />
        <DropZone
          file={this.props.value.file}
          updateFile={this.props.updateFile}
        />
        <Answers
          updateAnswers={this.props.updateAnswers}
          answers={this.props.value.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;

You shouldn't be passing props into state like here from the Question component:

this.state = {
  question: props.value.question,
  uniqueId: props.value.uniqueId,
  answers: props.value.answers,
  file: props.file
};

Because then you're allowing two different components to have two logical sources of state that they believe to be the same. Keep one source of truth that has everything that its children depend on. If all of the children components don't depend on the data (state), consider moving it down a level to the state of the child, otherwise pass as props.

Upvotes: 1

Related Questions