1

I'm a react newbie and I'm taking on a form that looks as follows: on the left side are the actual input fields and on the right there's a live preview of what the user is typing.

There are 2 input fields for the form name and what it's for and (that's the tricky part) a button that adds questions, each has 2 input fields for again a name and description.

I have 2 functions that manipulate the state, one that adds an object of empty strings for title and description to the questions array. Looks like this:

addQuestion = (event)=>{
    event.preventDefault();
    this.setState((prevState) => ({
      questions: [...prevState.questions, {questionTitle: "", questionDescription: ""}]
    }));
  }

The other one takes the user input value and sets it as the state for the title and description.

setQuestion(question){
    this.setState((prevState) => ({
      questions: [{questionTitle: this.questionTitle.value, questionDescription: this.questionDescription.value}]
    }));
  }

Now when I add more than 1 question and try to type into the input fields, those questions disappear, as the state gets set only for one object in the questions array. If I use a spread operator, that won't work because it's a live form, so the second function is called on change, therefore each keystroke equals a new question.

Here's the piece of code that maps over the state on change of the input fields:

const QuizQuestion = props=>{
  return(
    <div className="quiz-question">
      <h2 className="question-title">{props.questionTitle}</h2>
      <p className="question-description">{props.questionDescription}</p>
    </div>
  )
}


/*this is located in the render method*/
 {this.state.questions.map((element, i)=>(
            <QuizQuestion
            key={i}
            item={element}
            questionTitle={element.questionTitle}
            questionDescription={element.questionDescription}
            />
          ))}

What am I doing wrong? Thanks in advance for your help!

1 Answers1

0

In your example setQuestion() is called as your input is changed i.e. for onChange event handling. On each call setQuestion() repeatedly overwrite the question array of the state object. So, when the second form data is added to your first element of array then there is no issue, since there is initially no element in the question array. Hence, your first element of array is constantly overwritten and at last when you click on "Add Question" button then a new element is added to array by addQuestion().

The problem is that once you start typing the data in third form the setQuestion() method is called which updates your state and your question array is reset. Hence I merged I used ...prevstate.question to merge the previous state data with the new one.

But hold on this gives rise to another problem. Now you question array is filled with objects with empty value pairs { questionTitle: "", questionDescription: "" } each time you change the input value. However, there is no reflect of the current data you are entering in the state object. This is due to the fact that initially the value of all the inputs in your form is empty by default. So, when setQuestion is called on the change of input the previous objects are now recursively merged with the new objects. This leads to automatic & useless addition of element in array at press of each key. Also lost of new forms are added as a side effect (since you use map() on this.state.question to generate new forms)

So, I came up with a trick to overcome this. Just remove the last element of the question array of this.state using this.state.questions.splice(-1) before updating calling this.setState() to update state information.

See the code below. Here is the demo on codesandbox

import React, { Component } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

const QuizHead = props => {
  return (
    <div className="quiz-head">
      <h1 className="title">{props.title}</h1>
      <p className="description">{props.description}</p>
    </div>
  );
};
const QuizQuestion = props => {
  return (
    <div className="quiz-question">
      <h2 className="question-title">{props.questionTitle}</h2>
      <p className="question-description">{props.questionDescription}</p>
    </div>
  );
};

class App extends Component {
  constructor() {
    super();

    this.state = {
      title: "",
      description: "",
      questions: []
    };
    this.setQuestion = this.setQuestion.bind(this);
    this.setHead = this.setHead.bind(this);
    this.addQuestion = this.addQuestion.bind(this);
  }

  setHead = event => {
    event.preventDefault();
    this.setState({
      title: this.title.value,
      description: this.description.value,
      questions: []
    });
  };

  addQuestion = event => {
    event.preventDefault();
    console.log("addquestion prev state", this.state);
    this.setState(prevState => ({
      questions: [
        ...prevState.questions,
        { questionTitle: "", questionDescription: "" }
      ]
    }));
    this.state.questions.map((element, i) =>
      console.log(element.questionTitle)
    );
  };

  setQuestion(question) {
    this.state.questions.splice(-1);
    this.setState(prevState => ({
      questions: [
        ...prevState.questions,
        {
          questionTitle: this.questionTitle.value,
          questionDescription: this.questionDescription.value
        }
      ]
    }));
    console.log("set question", this.state);
  }

  render() {
    return (
      <div className="App">
        <article className="forms">
          <form className="head-form" onChange={this.setHead}>
            <input
              type="text"
              ref={titleInput => {
                this.title = titleInput;
              }}
            />
            <textarea
              name="description"
              ref={descriptionInput => {
                this.description = descriptionInput;
              }}
            />
          </form>
          <section className="questions-section">
          {console.log('Form state',this.state)}
            {this.state.questions.map((element, i) => (
              <form
                key={i}
                item={element}
                className="question-form"
                onChange={this.setQuestion}
              >
                <input
                  className="question-title"
                  type="text"
                  ref={questionTitleInput => {
                    this.questionTitle = questionTitleInput;
                  }}
                />
                <input
                  className="question-description"
                  ref={questionDescriptionInput => {
                    this.questionDescription = questionDescriptionInput;
                  }}
                  type="text"
                />
              </form>
            ))}
            <button className="add-question" onClick={this.addQuestion}>
              Add Question
            </button>
          </section>
        </article>
        <article className="final-quiz">
          <QuizHead
            title={this.state.title}
            description={this.state.description}
          />
          {this.state.questions.map((element, i) => (
            <QuizQuestion
              key={i}
              item={element}
              questionTitle={element.questionTitle}
              questionDescription={element.questionDescription}
            />
          ))}
        </article>
      </div>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

IMPORTANT NOTE

As per React Documentation a state should be treated as immutable object.

So, it is not appropraite to modify the elements of an array which is present as a property on this.state. You can reset it directly instead of modifying. However there are other ways to this. You can try Immutability Helpers

You can see this SO post for more detail about what I am saying

geeksal
  • 4,856
  • 3
  • 24
  • 47