Controlled vs Uncontrolled - Confusing Part
Adam C. |

Photo By: Duangphorn Wiriya

There are two types of “Controlled vs Uncontrolled” in React, and they are kind of “contradict” to each other. 

Here is what it says in Forms Docs:

In a controlled component, form data is handled by a React component. The alternative is uncontrolled components, where form data is handled by the DOM itself.

Use examples:

Controlled Component

class ControlledForm extends React.Component {
  state = {value: ''}; // controlled by React state
  
  handleChange = (event) => {
    this.setState({value: event.target.value});
  }
  
  handleSubmit = (event) => {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" 
          	value={this.state.value} 
          	onChange={this.handleChange} 
          />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

As we see in the above example, the input value is controlled by ‘state’, when input changes, it sets state, then component re-renders with the new value set.

Uncontrolled Component

class UncontrolledForm extends React.Component {
  constructor(props) {
    super(props);
    this.input = React.createRef();
  }

  handleSubmit = (event) => {
    alert('A name was submitted: ' + this.input.current.value);
    event.preventDefault();
  };

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" ref={this.input} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

As we see from the above example, there is no state for the input value, instead, it uses Ref to reference the input element. And the DOM handles the input change, and when the form is submitted, it uses the ref, this.input to get the input value.

Pretty straightforward, right? we have no issue here. 

But when we read another doc about Derived State:

The terms “controlled” and “uncontrolled” usually refer to form inputs, but they can also describe where any component’s data lives. Data passed in as props can be thought of as controlled (because the parent component controls that data). Data that exists only in internal state can be thought of as uncontrolled (because the parent can’t directly change it).

Use examples:

Controlled Component

function EmailInput(props) {
  return <input onChange={props.onChange} value={props.email} />;
}

Okay, we call the above component is controlled component because its value is from the parent component, and is changed by its parent's function ‘props.onChange’.  - Fully controlled component!

Uncontrolled Component

class EmailInput extends Component {
  state = { email: this.props.email };

  render() {
    return <input onChange={this.handleChange} value={this.state.email} />;
  }

  handleChange = event => {
    this.setState({ email: event.target.value });
  };
}

This could be a confusing part, we call this uncontrolled component because it's not controlled by it's parent. It does handle the input value in the component except for the initial value, which is assigned from its parent.  Did we just learned in the first example, that the Controlled Component means the value is controlled by “state” instead of the DOM. Yes, but it's a different context, so please don't be confused. 

Controlled & Uncontrolled Warnings 

Below is one  of the common warnings we may see in the browser Console when debugging the React App:

Warning: A component is changing a controlled input of type checkbox 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.

This refers to the first case, i.e.,  the state controlled vs the DOM controlled. For example, if we initialize the input value to be null, then try to set it via state, the debugger will complain about the error.

class ControlledForm extends React.Component {
  state = {value: null}; // set it to null, will make the input uncontrolled
  
  handleChange = (event) => {
   //WARNING: try to switch from uncontrolled to controlled
    this.setState({value: event.target.value}); 
  }

  render() {
    return (
      <form>
        <label>
          Name:
          <input type="text" 
          	value={this.state.value} 
          	onChange={this.handleChange} 
          />
        </label>
      </form>
    );
  }
}

But sometimes the debugger has a bug, like the below example:

const { classes, block, state, handleToggle } = this.props;

return(    
<FormControlLabel
	control={
	<Checkbox
		tabIndex={-1}
		onClick={handleToggle}
		name={block.title === "Border" ? "isBorder" : "isTextWrap"}
		checked={
			block.title === "Border"
			? state.isBorder
			: state.isTextWrap
		}
	/>
	}
	classes={{
		label: classes.label,
	}}
/>);

Although “state” is from props, the debugger thinks it's controlled by the local state for whatever reason. The warnings are gone after modifying the checked logic as

checked={
   block.title === "Border"
     ? (state.isBorder
       ? true
       : false)
     : (state.isTextWrap
     ? true
     : false)
}