Transitioning from uncontrolled inputs to controlled
25 Jul 2017
You know about the differences between the controlled vs. uncontrolled form inputs.
You may have started out with uncontrolled form inputs — which is perfectly fine!
It turns out your requirements have got more sophisticated. You need to disable that submit button or instantly validate your fields, perhaps.
And for that, you need your inputs to be controlled.
So how do you go from uncontrolled to controlled?
The transition is straightforward. You would need to:
- Identify all form controls — textboxes, selects, checkboxes.
- Initialize the state for each of these.
- Make these controls "get" their value from
this.state
. - Make these controls "set" new values to
this.state
— using theonChange
prop. - Change your submit handler to get values from the state, instead of
ref
s.
Suppose you had a form with uncontrolled inputs, something like this:
class UncontrolledForm extends Component {
handleSubmit = () => {
const email = this._emailInput.value;
const agreeCheckbox = this._agreeCheckbox.checked;
...
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="email" ref={inp => this._emailInput = inp} />
<input type="checkbox" ref={inp => this._agreeCheckbox = inp} />
</form>
);
}
}
1. Identify all form controls
As simple as it sounds, you are just taking a mental (or paper, or code) note of what your form has. It's important that you give each control a meaningful name. "Email field" instead of just "a text field".
In the example above, the form controls are:
- an email field;
- an "agree to terms" checkbox.
2. Initialize the state for these
You now have a list of what's inside your form.
The next step would be to start storing the values of these things in this.state
, instead of relying on the browser to store these for us in DOM.
Don't worry about how these will be used or updated just yet.
Continuing the example, we would need the state to contain:
- a string with the value of the email field;
- a boolean indicating whether the user agrees to the terms.
In code, we would set it in the constructor
like this:
class ControlledForm extends Component {
constructor() {
super();
this.state = {
email: "",
agree: false,
};
}
}
3. Make the controls "get" their values from state
We are now kind of storing the values of these controls inside this.state
.
Now it's time to tell our inputs to use these values.
class ControlledForm extends Component {
// constructor omitted
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="email" value={this.state.email} />
<input type="checkbox" checked={this.state.agree} />
</form>
);
}
}
Why's it that we are using checked
for the checkbox, but value
for the text inputs?
What would you use for textareas, or selects, or radio buttons?
Refer to the "What makes an input controlled" section of Controlled vs. uncontrolled inputs in React.
4. Make the controls "set" their values to state
An input that shows a static value and cannot be changed isn't of much value.
So we need to allow the inputs to update the state back.
If you need a refresher, here's an excerpt from Controlled vs. uncontrolled inputs in React:
Every time you type a new character,
handleNameChange
is called. It takes in the new value of the input and sets it in the state.
It starts out as an empty string —
''
.You type
a
andhandleNameChange
gets ana
and callssetState
. The input is then re-rendered to have the value ofa
.You type
b
.handleNameChange
gets the value ofab
and sets that to the state. The input is re-rendered once more, now withvalue="ab"
.This flow kind of 'pushes' the value changes to the form component, so the
Form
component always has the current value of the input, without needing to ask for it explicitly.This means your data (state) and UI (inputs) are always in sync. The state gives the value to the input, and the input asks the
Form
to change the current value.
We are going to write an event handler for every input, and pass it as the onChange
prop.
class ControlledForm extends Component {
// contructor omitted
handleEmailChange = (evt) => {
this.setState({ email: evt.target.value });
};
handleAgreeChange = (evt) => {
this.setState({ agree: evt.target.checked });
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="email" value={this.state.email} onChange={this.handleEmailChange} />
<input type="checkbox" checked={this.state.agree} onChange={this.handleAgreeChange} />
</form>
);
}
}
5. Change your submit handler to get values from state, instead of ref
s
Now for the easiest part, instead of reading from ref
s in handleSubmit
, we will simply get from the state.
class ControlledForm extends Component {
handleSubmit = () => {
const email = this.state.email;
const agree = this.state.agree;
...
};
}
All in all, the new, controlled form would look like this:
class ControlledForm extends Component {
constructor() {
super();
this.state = {
email: '',
agree: false,
};
}
handleSubmit = () => {
const email = this.state.email;
const agree = this.state.agree;
...
};
handleEmailChange = (evt) => {
this.setState({ email: evt.target.value });
};
handleAgreeChange = (evt) => {
this.setState({ agree: evt.target.checked });
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="email" value={this.state.email} onChange={this.handleEmailChange} />
<input type="checkbox" checked={this.state.agree} onChange={this.handleAgreeChange} />
</form>
);
}
}
And we can now have the most up-to-date values right in the state, enabling us to do much more than uncontrolled inputs would allow.