15
votes

I'm creating a simple form with react using controlled inputs. In my component state, I have 2 properties "clientName" and "license" on state. changing those works fine. But there's a "shipping" property that is an object. Changing any of the shipping properties gives an error. For example, if i change "address1", as soon as the state is set in the handleShippingChange function, I get the error:

Warning: TextField 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 suspect it has something to do with how I'm defining the value for those TextFields and how I'm setting the state for shipping properties. What am I doing wrong? The code for my component is below:

import React, {Component} from 'react';
    import TextField from 'material-ui/TextField';
    import RaisedButton from 'material-ui/RaisedButton';
    import 'whatwg-fetch';

    class Clients extends Component {
      constructor() {
        super();
        this.state = {
          "clientName": "client name",
          "shipping": {
            "name": "name",
            "address1": "address 1",
            "address2": "address 2",
            "city": "city",
            "state": "state",
            "zip": "zip",
            "country": "country"
          },
          "license": "license"
        };

        this.handleChange = this.handleChange.bind(this);
        this.handleShippingChange = this.handleShippingChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
      };       

      handleChange(event) {
        this.setState({
          [event.target.name]: this.getFieldValue(event.target)
        });
      };

      handleShippingChange(event) {
        this.setState({
          shipping: {
            [event.target.name]: this.getFieldValue(event.target)
          }
        });
      };

      getFieldValue(target) {
        return target.type === 'checkbox' ? target.checked : target.value;
      };

      handleSubmit = (event) => {
        event.preventDefault();

        // do some stuff
      };   

      render() {
        return <div>
          <h1>
            Clients Page
          </h1>

          <form onSubmit={this.handleSubmit}>
            <TextField
              hintText="Enter the client name"
              floatingLabelText="Client Name"
              value={this.state.clientName}
              onChange={this.handleChange}
              name="clientName"
            />
            <h2>Shipping Info</h2>
            <TextField
              hintText=""
              floatingLabelText="Name"
              value={this.state.shipping.name}
              onChange={this.handleShippingChange}
              name="name"
            />
            <br />
            <TextField
              hintText=""
              floatingLabelText="Address Line 1"
              value={this.state.shipping.address1}
              onChange={this.handleShippingChange}
              name="address1"
            />
            <br />
            <TextField
              hintText=""
              floatingLabelText="Address Line 2"
              value={this.state.shipping.address2}
              onChange={this.handleShippingChange}
              name="address2"
            />
            <br />
            <TextField
              hintText=""
              floatingLabelText="City"
              value={this.state.shipping.city}
              onChange={this.handleShippingChange}
              name="city"
            />
            <br />
            <TextField
              hintText=""
              floatingLabelText="State"
              value={this.state.shipping.state}
              onChange={this.handleShippingChange}
              name="state"
            />
            <br />
            <TextField
              hintText=""
              floatingLabelText="Zip Code"
              value={this.state.shipping.zip}
              onChange={this.handleShippingChange}
              name="zip"
            />
            <br />
            <TextField
              hintText=""
              floatingLabelText="Country"
              value={this.state.shipping.country}
              onChange={this.handleShippingChange}
              name="country"
            />
            <br />
            <TextField
              hintText=""
              floatingLabelText="License"
              value={this.state.license}
              onChange={this.handleChange}
              name="license"
            />
            <br />
            <RaisedButton label="OK" primary={true} type="submit" />
          </form>
        </div>
      };
    }

    export default Clients;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
3

3 Answers

46
votes

Controlled/Uncontrolled input means if the <input> field has a value or not.

// This is a controlled input
<input value="foo"/>

// This is an uncontrolled input
<input value={null}/>

The idea is that you don't want to change from a controlled to an uncontrolled input. Both types of input acts differently and this could potentially lead to bugs and/or inconsistency.

The easiest fix is to ensure there's always a default value (in the case of an empty field, the default would be an empty string '').

Also, note that consistent types are usually better than nullable types given you have a guarantee on the type of a certain value. This helps a lot in reducing overhead due to null checks (if (val != null) { /* ...etc */ })

But if you just want a one line fix, you can also provide the default value inline in jsx:

<input value={value || ''}/>
4
votes

The underlying problem is that the way I'm setting a property on the shipping object, it does not merge the new property value with the original property values. Therefore the warning wasn't for the TextField that i was EDITING, it was for the other shipping TextFields that were getting blown away. I'm not sure if this is accepted practice since it's hard to find examples where you're working with an object in the state. However, changing the handleShipping method to this fixed my problem:

handleShippingChange(event) {
  var shipping = this.state.shipping;
  shipping[event.target.name] = this.getFieldValue(event.target);

  this.setState({
    shipping: shipping
  });
};

Basically, I'm creating a copy of the existing shipping object from the state, altering it and setting the entire shipping object equal to the altered copy.

1
votes

I just encountered the same issue but it turns out the error was something much simpler (borderline embarrassing).

I forget to add the event parameter when I define my onChange function.

handleChange(event) {
  ...          ^-this
};

Strange that I didn't get a event is undefined error... Hope this helps someone.