Complete guide to controlled components in React

Image from instagram

What are controlled components

Controlled components, simply put, are form input elements that have their value mapped to the component state.
This means that any change in the input element data will change the component state and vice versa.

From the React Documentation:

In most cases, we recommend using controlled components to implement forms. 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.

For Example:

  • When a user inputs an email in a form, the email value will be stored in the component state. Then at submission, you can read the email given by the user from the state itself.
  • If you want to auto fill or pre fill some input field, just update the state and that input element will be updated automatically.

It isn’t the only way of managing the form data, the opposite of controlled components are uncontrolled components where you don’t map any state to form inputs. They work the same way traditional HTML forms work. Nothing react-y in them.

You can get the input values in uncontrolled components using React refs, which we’ll briefly discuss.

Why to use controlled components

There are many practical use cases for using controlled components over uncontrolled ones. Although there can be many cases where it makes more sense to go with uncontrolled components like when migrating a non-react codebase to react, using inputs that don’t support controlled components.

  • Do form validation while the user is typing out the form. Traditionally you’d have the user submit the form, do the validations and then show the errors in the form, if any. But using controlled components you can do this on the go, while the user is typing it out.
  • Maintain a specific input format. For example make the user input date in a specific format, complete will spaces and dashes.
  • Prevent incomplete form submissions. You can just disable the submit button till all necessary fields are filled. Using controlled components you can do this easily as all you have to do is check the component state and enable/disable the submit button.
  • Dynamic input fields. If the number of input fields are not fixed and user can add more input fields.

How to create an Uncontrolled component

The one where the form data is handled by the DOM itself and we use React Refs to get the data.

class MyUnControlledComponent extends React.Component {
  constructor(props) {
    super(props);

    this.handleSubmit = this.handleSubmit.bind(this);
    this.emailRef = React.createRef();
  }

  handleSubmit(event) {
    console.log("The email address is: " + this.emailRef.current.value);
    event.preventDefault();
  }

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

So now imagine if you have 10 fields in your form, you’d have to create 10 refs and bind each to their corresponding input elements. I think there should be a better approach than this.

How to create Controlled components

A controlled component takes a ‘value’ prop and a callback ‘onChange’. The value passed to the ‘value’ prop is the state in that component or its parent component. ‘onChange’ prop takes a callback function which gets called every time the user makes any change to the input element and it updates the state on every change.

class MyControlledComponent extends React.Component{
	constructor{
		super(props)
		this.state = {
			email:"",
		}
	}

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

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

The Flow

Default input value is binded from the state -> User updates the input -> onChange updates the state -> input element shows the updated state

Everything happens in ‘real time’ so to speak.
You’re not changing the input value, you’re re-rendering the input element based on its updated ‘value’ prop.

Simplify the above code using Hooks

Here we’re using the useState hook to maintain the state in a functional component. Nothing fancy and completely optional.

function Form()=>{
	const [email, setEmail]= React.useState("");

	return(
		<input type="text" value={email}
		onChange={(event)=>{setEmail(event.target.value)}} />
	)
}

Controlled components for other input element types

Checkboxes and Radio buttons

function Form()=>{
	const [checked, setChecked]= React.useState(false);

	return(
		<input type="checkbox" checked={checked} onChange={(event)=>{
			setChecked(event.target.checked)
		}} />
	)
}

Select

function Form()=>{
	const [option, setOption]= React.useState("");

	return(
		<select value={option} onChange={(event)=>{setOption(event.target.value)}}>
            <option value="option1">Option 1</option>
            <option value="option2">Option 2</option>
            <option value="option3">Option 3</option>
            <option value="option4">Option 4</option>
        </select>
	)
}

Textarea

Same as input type “text”.

function Form()=>{
	const [longertext, setLongertext]= React.useState("");

	return(
		<textarea value={longertext}
		onChange={(event)=>{setLongertext(event.target.value)}} />
	)
}

Exceptions

The file input tag

The file input: The “file” input type will always be uncontrolled because its value can only be set by the user and not programmatically. Your program can’t know which file to choose and so can’t set its value on its own. You can handle input type file using React refs:

...
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.fileInput = React.createRef();
  }
  handleSubmit(event) {
    event.preventDefault();
	console.log("Selected file is: " + this.fileInput.current.files[0].name);
  }
...

Conclusion

We’ve covered everything we need to know to implement controlled and uncontrolled components here. We can use this knowledge to implement complex use cases like:

  • Forms with dynamic input values
  • Handling multiple input values using the same onChange callback
  • Create form validations on the go