mtrics
mtrics

Reputation: 25

React form - Field is automatically unselected when value changes

I'm a total beginner in React, and am trying to build my first form.

I fetch the questions asked in said form using an API, and once the form submitted, I would like to create an answer object through this same API.

The problem is that everything seems to work just fine (questions render ok, answer object is updated in state), but everytime the value of a field change, I have to re-select the field to keep typing. Basically, it's like I'm auto-clicked away from the field when the value changes.

Here's what happens :

enter image description here

And here's a snippet of (probably) the offending code :

class Form extends React.Component {
    constructor(props) {
        super(props);
        {/* Questions are filled on page load, answers is what I'm working with */}
        this.state = { questions: [], answers: [] }; 
        this.handleChange = this.handleChange.bind(this);
    }


    // This is where the magic is supposed to happen
    handleChange(event) {
        let key = event.target.id,
            value = event.target.value;

        {/* Here, my goal is to build an object with questions ids as keys and values of fields as key values. */}

        this.setState(prevState => {
            let copy = Object.assign({}, prevState.answers);
            copy[key] = value;
            return {  answers: copy };
        })

    }

    render() {
        const { questions } = this.state;

        const TextareaInput = (fieldId) => (
            <div>
                <textarea name={ fieldId.fieldId } value={this.state.answers[fieldId.fieldId]} id={ fieldId.fieldId } onChange={this.handleChange}  ></textarea>
            </div>
        );

        const TextInput = (fieldId) =>(
            <div>
                <input type='text' name={ fieldId.fieldId } value={this.state.answers[fieldId.fieldId]} id={ fieldId.fieldId } onChange={this.handleChange} />
            </div>
        );

        const allQuestions =  (
            questions.map((q, key) =>
                <div key={q.id} className='question'>
                    <label htmlFor={ q.field_id } className={ q.required ? 'required' : '' } dangerouslySetInnerHTML={{__html: q.question}}></label>

                    {q.field_type == 'text' ? <TextInput fieldId={q.field_id}/> : <TextareaInput fieldId={q.field_id}/>}
                </div>
            )
        )

        return (
            <form>
                { allQuestions }
            </form>
        )
    }

}
export default Form;

(Full component on pastebin)

I think the problem comes from my handleChange function, but I'm not sure what could be causing this. I tried adding some stuffs and moving things around a little without any luck...

Upvotes: 0

Views: 505

Answers (2)

Zachary Haber
Zachary Haber

Reputation: 11037

You need to call the TextInput and TextareaInput like functions instead of using them like separate components since you defined them within the component.

    {q.field_type == 'text'
      ? TextInput(q.field_id)
      : TextareaInput(q.field_id)}

React was unable to keep the reference to them straight and seemingly considered them different elements every render.

Also, as I'm sure you are already aware, you should be careful using dangerouslySetInnerHTML as the name implies, it can be dangerous.

class Form extends React.Component {
  constructor(props) {
    super(props);
    {
      /* Questions are filled on page load, answers is what I'm working with */
    }
    this.state = {
      questions: [
        {
          id: 2,
          field_id: 2,
          question: 'How are you today?',
          field_type: 'text',
        },
                {
          id: 3,
          field_id: 3,
          question: 'What\'s the answer to life, the universe, and everything??',
          field_type: 'textarea',
        },
      ],
      answers: [],
    };
    this.handleChange = this.handleChange.bind(this);
  }

  // This is where the magic is supposed to happen
  handleChange(event) {
    let key = event.target.id,
      value = event.target.value;

    {
      /* Here, my goal is to build an object with questions ids as keys and values of fields as key values. */
    }

    this.setState((prevState) => {
      let copy = Object.assign({}, prevState.answers);
      copy[key] = value;
      return { answers: copy };
    },()=>{console.log(this.state)});
  }

  render() {
    const { questions } = this.state;

    const TextareaInput = (fieldId) => (
      <div>
        <textarea
          name={fieldId}
          value={this.state.answers[fieldId]}
          id={fieldId}
          onChange={this.handleChange}
        ></textarea>
      </div>
    );

    const TextInput = (fieldId) => (
      <div>
        <input
          type="text"
          name={fieldId}
          value={this.state.answers[fieldId]}
          id={fieldId}
          onChange={this.handleChange}
        />
      </div>
    );

    const allQuestions = questions.map((q, key) => (
      <div key={q.id} className="question">
        <label
          htmlFor={q.field_id}
          className={q.required ? 'required' : ''}
          // As I'm sure you are already aware, this is likely a terrible idea.
          dangerouslySetInnerHTML={{ __html: q.question }}
        ></label>
        {q.field_type == 'text'
          ? TextInput(q.field_id)
          : TextareaInput(q.field_id)}
      </div>
    ));

    return <form>{allQuestions}</form>;
  }
}

ReactDOM.render(<Form/>, document.querySelector('#root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

<div id="root" />


In order to avoid using dangerouslySetInnerHTML for the questions, I'd suggest using some sort of markdown renderer. It should be a good enough for most use cases in asking questions.

https://www.npmjs.com/package/react-markdown

https://www.markdownguide.org/

class Form extends React.Component {
  constructor(props) {
    super(props);
    {
      /* Questions are filled on page load, answers is what I'm working with */
    }
    this.state = {
      questions: [
        {
          id: 2,
          field_id: 2,
          question: 'How are *you* today?',
          field_type: 'text',
        },
                {
          id: 3,
          field_id: 3,
          question: 'What\'s the **answer** to life, the universe, and everything??',
          field_type: 'textarea',
        },
        {id: 4,
        field_id: 4,
        field_type: 'text',
        question:`# This is the big question
#### *ARE YOU READY?*
1. Is this the real life?
1. Or is this just fantasy?
`
        }
      ],
      answers: [],
    };
    this.handleChange = this.handleChange.bind(this);
  }

  // This is where the magic is supposed to happen
  handleChange(event) {
    let key = event.target.id,
      value = event.target.value;

    {
      /* Here, my goal is to build an object with questions ids as keys and values of fields as key values. */
    }

    this.setState((prevState) => {
      let copy = Object.assign({}, prevState.answers);
      copy[key] = value;
      return { answers: copy };
    },()=>{console.log(this.state)});
  }

  render() {
    const { questions } = this.state;

    const TextareaInput = (fieldId) => (
      <div>
        <textarea
          name={fieldId}
          value={this.state.answers[fieldId]}
          id={fieldId}
          onChange={this.handleChange}
        ></textarea>
      </div>
    );

    const TextInput = (fieldId) => (
      <div>
        <input
          type="text"
          name={fieldId}
          value={this.state.answers[fieldId]}
          id={fieldId}
          onChange={this.handleChange}
        />
      </div>
    );

    const allQuestions = questions.map((q, key) => (
      <div key={q.id} className="question">
        <label
          htmlFor={q.field_id}
          className={q.required ? 'required' : ''}
        ><ReactMarkdown source={q.question}/></label>
        {q.field_type == 'text'
          ? TextInput(q.field_id)
          : TextareaInput(q.field_id)}
      </div>
    ));

    return <form>{allQuestions}</form>;
  }
}

ReactDOM.render(<Form/>, document.querySelector('#root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-markdown/4.3.1/react-markdown.js" integrity="sha256-4jDgUokdWbazrdnMjWm+TftvBFnOwSNIpvKhgYsInfw=" crossorigin="anonymous"></script>

<div id="root" />


If you need full capabilities of rendering:

https://pragmaticwebsecurity.com/files/cheatsheets/reactxss.pdf, gives an example to use DOMPurify to sanitize the input coming in to prevent cross site scripting and other dangerous behavior rendering outside html can give.

So, for that, you'd purify the string and then pass it into dangerouslySetInnerHTML once it's purified.

Upvotes: 3

Sebastian B.
Sebastian B.

Reputation: 2211

I think the questions get lost from your state due to the setState call. AFAIK if setState is given an updater function, it must return the whole state. Shallow merging is only supported if an object is given.

this.setState(prevState => ({
    ...prevState,
    answers: {
        ...prevState.answers,
        [key]: value;
    }
}))

(I hope object spread is supported in your environment, If not, do yourself a favor ;-))

Upvotes: 0

Related Questions