devamat
devamat

Reputation: 2513

ReactJS: update a controlled input field problem

I have been reading some threads on SO but I could not figure out how to solve this issue or why it's happening. Can someone explain it like I'm 5?

Warning: A component is changing a controlled input of type text to be uncontrolled. Input elements should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component

I am developing a lesson creator, and the user must be able to open an existing lesson, hence the input fields must be programmatically filled with the content of the existing lesson.

My constructor:

  constructor(props) {

    super(props);

    this.state = {
                  lessonID: -1,
                  sectionsArray: [],
                  title: 'No title',
                  type: 'r',
                  language: 'gb',
                  book: 'booka',
                  level: '1',

                  loading: false,

                  saved: true,

                  messageBox: '',

                  lessonOpenModal: false,

    }

    this._state = this.state;

    this.updateSectionsFromChild = this.updateSectionsFromChild.bind(this);

    this.sectionAdd = this.sectionAdd.bind(this);
    this.sectionRemove = this.sectionRemove.bind(this);

    this.menuInput = this.menuInput.bind(this);
    this.menuDropDown = this.menuDropDown.bind(this);

    this.lessonCreate = this.lessonCreate.bind(this);
    this.lessonSave = this.lessonSave.bind(this);
    this.lessonDelete = this.lessonDelete.bind(this);
    this.lessonOpen = this.lessonOpen.bind(this);

    this.sections = [];

    }

This are the functions that update the controlled components:

  menuDropDown(event, data) {
    this.setState({
      [data.name]: data.value,
      saved: false,
    });

    console.log(data.name);
    console.log(data.value);
  }

  menuInput(event) {
    this.setState({
      [event.target.name]: event.target.value,
      saved: false,
    });
}

And then this is the part of code that retrieves the lesson and tries to update the state:

  async openLesson(lessonID) {
    await ARLessonOpen(lessonID).then((result) => {

      this.setState(this._state);

      this.setState({
        id: result.lesson.id,
        language: result.lesson.language,
        book: result.lesson.book, // this is a drop down, and it's not causing errors
        type: result.lesson.type, // this is a drop down, and it's not causing errors
        level: result.lesson.level, // this is a drop down, and it's not causing errors
        title: result.lesson.title, // this is an input, and IT'S THE ISSUE

        sectionsArray: result.sections.map((section, i) => ({
          key: i,
          id: i,
          title: section.title,
          duration: section.duration,
          content: section.content,
        }))
      })

    }).catch(function(error) {
      console.log(error);
    });
  }

The only field that is not working is the 'title' and I can't understand why. How can I update the input value programmatically?

JSX:

  renderSections = () => {

    if (this.state.sectionsArray.length > 0) {
      return this.state.sectionsArray.map((section, i) =>
        <LessonSection
          key={section.id}
          id={section.id}
          title={section.title}
          duration={section.duration}
          content={section.content}
          sectionRemove={this.sectionRemove}
          sectionAdd={this.sectionAdd}
          updateSectionsFromChild={this.updateSectionsFromChild}
        />
      )
    } else {
      return (
        <div style={{color: 'black'}}> 
          <Button
            size='mini'
            icon='plus square outline'
            onClick={this.sectionAdd} />
          Add a section to start creating your lesson.
        </div>
      )
    }
  }

  render() {

    return (
      <div className='Lesson-editor'>

        {this.state.messageBox}

        <div style={{display: 'none'}}>

          <DefaultLoader
            active={this.state.loading}
            message={this.state.message}
          />

        </div>

        <div className="Lesson-editor-menu Small-font">

          <div className="Menu-buttons">

            <Button
              size='mini'
              icon='plus square outline'
              onClick={this.sectionAdd} />

            <Button
              size='mini'
              icon='file outline'
              onClick={this.lessonCreate} />

            <DialoglessonOpen
              open={this.state.lessonOpenModal}
              actionOnLessonSelected={(lessonID) => this.openLesson(lessonID)}

              onCancel={() => this.setState({lessonOpenModal: false})} />

            <Button size='mini' icon='open folder outline' text='Open lesson' description='ctrl + o' onClick={this.lessonOpen} />

            <Button
              size='mini'
              icon='save outline'
              onClick={this.lessonSave} />

            <Button
              size='mini'
              icon='delete'
              onClick={this.lessonDelete} />

            <Button
              size='mini'
              icon='delete'
              color='red'
              onClick={ARClearTables} />

          </div>

          <Input
            className='title'
            fluid
            placeholder='Lesson title'
            value={this.state.title}
            name='title'
            onChange={this.menuInput}
          />

          <div>

            <Dropdown
              fluid
              compact
              placeholder='Language'
              search
              selection
              options={lessonLanguages}
              //defaultValue='gb'
              value={this.state.language}
              name='language'
              onChange={this.menuDropDown}
            />

            <Dropdown
              fluid
              compact
              placeholder='Book'
              search
              selection
              options={lessonBooks}
              //defaultValue='booka'
              value={this.state.book}
              name='book'
              onChange={this.menuDropDown}
            />

            <Dropdown
              fluid
              compact
              placeholder='Lesson type'
              search
              selection
              options={lessonTypes}
              defaultValue='r'
              name='type'
              onChange={this.menuDropDown}
            />

            <Dropdown
              fluid
              compact
              placeholder='Lesson level'
              search
              selection
              options={lessonLevels}
              defaultValue='1'
              name='level'
              onChange={this.menuDropDown}
            />

          </div>

        </div>

        <div className='Sections'>
          { this.renderSections() }
        </div>

      </div>
    );
  }
}

Upvotes: 2

Views: 638

Answers (2)

devamat
devamat

Reputation: 2513

I figured it out: the problem is that there was an error in my code. I was assigning a null value to the input field value in state.

 async openLesson(lessonID) {
    await ARLessonOpen(lessonID).then((result) => {

      this.setState(this._state);

      this.setState({

        /* HERE: I try to access result.lesson but it's null! I should
            use result.lesson[0]. So the problem is that I was 
            assigning a null value to the input field resulting in the error */

        id: result.lesson.id,
        language: result.lesson.language,
        book: result.lesson.book,
        type: result.lesson.type,
        level: result.lesson.level, 
        title: result.lesson.title,

        sectionsArray: result.sections.map((section, i) => ({
          key: i,
          id: i,
          title: section.title,
          duration: section.duration,
          content: section.content,
        }))
      })

    }).catch(function(error) {
      console.log(error);
    });
  }

Upvotes: 0

Aminadav Glickshtein
Aminadav Glickshtein

Reputation: 24650

The initial value of input forms fields cannot be undefined or null, if you want to control it later. It should be an empty string. If you provide an undefined or null it's uncontrolled component.

In you code, React doesn't see any value to the input fields, so React believe it is a un-controlled component on the first mount. Later, when you add a value to the component React warning you that you can't give a value (controlled component) after you didn't provided a value (uncontrolled component)

Upvotes: 3

Related Questions