React provides two ways to use its form inputs, Controlled and Uncontrolled.
I will try to explain both of them and the user cases, after that I will introduce an in-the-middle way to control your form input: Loos-Controlled, which tries to fulfill the user case that the others do not solve well.
Uncontrolled form input
An uncontrolled form input is an input that is not controlled, which means it has its state to manage its current value coordinate to user’s interaction.
A form input without the property value
is a untrolled form input:
<input name="name" type="text" />
It will behave just like a normal form control, you can type anything you can, and it will updates as you type.
In most cases you may want to specified a default value for the input to have when it is rendered the first time:
<input name="name" type="text" defaultValue={'hello'} />
Uncontrolled form input works quite well in most cases; you only need to add an event handler to update your app state from the user input:
handleOnChange(event) {
this.setState({
value: event.target.value,
});
}
render() {
return (<div>
<div>Current Text: {this.state.value}</div>
<input
name="name"
type="text"
onChange={this.handleOnChange}
defaultValue="hello" />;
</div>);
}
Controlled form input
In some user cases, you need to take a full control of the value of the input.
Image you are asked to build a search bar with a suggestion list, like Google search:
According to the design your designer gave it to you, you are considering all the functionalities:
- User should be able to input text freely
- Suggestions will show according to the current user input
- User can use arrow key or mouse click to choose a suggestion to make the input change automatically
For features (1) and (2), you can use an uncontrolled form input to implement it easily, but you will fail when you try to implement (3), like:
handleOnChange(value) {
this.setState({
value,
});
}
handleSuggestionSelect(value) {
this.setState({
value,
});
}
render() {
return (<div>
<input
name="name"
type="text"
onChange={this.handleOnChange}
defaultValue={this.state.value} />;
<Suggestion query={this.state.value} onSelect={this.handleSuggestionSelect} />
</div>);
}
The logic in this code snap looks good. First, using an uncontrolled form input you leave the user to type whatever he wants, (1) is done. Then, by handling onChange
, you update state.value
to the latest user input, and you pass the value to <Suggestion />
so that it can update its suggestions, which achieves (2). You also give the state.value
back to input using defaultValue={this.state.value}
, but this time, it failed.
Since it is an uncontrolled form, so defaultValue
only effect when the input is rendered at the first time.
So what you need is a controlled form input, and you need to use value
instead of defaultValue
:
<input
name="name"
type="text"
onChange={this.handleOnChange}
value={this.state.value} />;
That’s it! The only difference is just replacing defaultValue
to value
. You may still not quite clear why this is called “controlled“.
Change the code a little bit (remove the onChange handler):
<input
name="name"
type="text"
value={this.state.value} />;
And now open the page and type to type something into the input!
Wherever you type does not go into the input! (let’s assume the initial value of this.state.value
is empty). Why? Because you specify the value of the input should be this.state.value
:
value={this.state.value}
Which means the value of the input is controlled by this.state.value
, it will always equal to this.state.value
unless you change it, and since we remove the handler for onChange
, so the state never gets the chance to update, that’s whatever you type does not work.
How to choose?
The choice between uncontrolled and controlled form input is quite simple:
- In some cases, if you need to update the value according to some business logic instead of user’s direct input, use controlled form input.
- Otherwise, use uncontrolled form input.
Not everyone is happy
Everything looks good already, any other problem?
Let’s get back to the suggestion example.
In a real application, your suggestions may not come from your local, but from a remote HTTP request, and every time user update the value a new request will be sent out to get the latest suggestions.
It sounds quite normal, but think about it: a programmer who want to search something about Javascript
, so he is going to type Javascript
, there actually will be multiple requests sending out with all the queries:
J
Ja
Jav
Java
Javas
Javasc
- …
Too much right? A better way will be that we set a small delay to send the query. Whenever a change happens, we don’t send out a new request directly. Instead, we wait for a certain short time, and if within the short time, another change comes, we get rid of the previous change, and wait for another short period. If no change comes within the time limit, we send out the request.
According to how long the delay you set, the sent out request queries will be reduced to like:
J
Java
Javascr
Javascript
Much better, right? OK let’s keep on working the code, we use the debounce
from Lodash to implement this kind of delay call:
constructor(props) {
super(props);
this.handleOnChange = debounce(this.handleOnChange.bind(this), 100);
}
handleOnChange(value) {
this.setState({
value,
});
}
handleSuggestionSelect(value) {
this.setState({
value,
});
}
render() {
return (<div>
<input
name="name"
type="text"
onChange={this.handleOnChange}
value={this.state.value} />;
<Suggestion query={this.state.value} onSelect={this.handleSuggestionSelect} />
</div>);
}
this line of code implemented the delay:
this.handleOnChange = debounce(this.handleOnChange.bind(this), 100);
Which will cause if multiple calls of this.handleOnChange
happens within 100ms, only the last one will be executed.
Looks great! Now you open the page, and try to type something, things become wired again!
After you type, the value of the input will take a 100ms delay to update!
Why? Because you debounce the change handler, which delayed the update of this.state.value
.
So in order to make the update of input value immediately and delay the update of the <Suggestions />
‘s query, you perhaps change you code to:
constructor(props) {
super(props);
this.updateSuggestQuery = debounce(this.updateSuggestQuery, 100);
}
updateSuggestQuery(query) {
this.setState({
query,
});
}
handleOnChange(value) {
this.setState({
value,
});
this.updateSuggestQuery(value);
}
handleSuggestionSelect(value) {
this.setState({
value,
});
}
render() {
return (<div>
<input
name="name"
type="text"
onChange={this.handleOnChange}
value={this.state.value} />;
<Suggestion query={this.state.query} onSelect={this.handleSuggestionSelect} />
</div>);
}
By doing this, the value of the input will update immediately, and the update for this.state.query
will be delayed!
But still several extra lines of code to write!
What if…, what if we can have some kind of input, that can be input freely, but also can update its value when a new value
is specified?
Loose-Controlled form input.
Let’s image what the behaviors of a loose-controlled form input will have:
- Will act just as an uncontrolled form input. Whenever you type something, it will update immediately!
- When you need to specify a specific value at any moment, you can specify it by giving a
value
prop, and it will update itself! - After update with a specific
value
, if the user continues to type, it will update immediately again.
As you can see, the behavior is kind of in the middle of controlled and uncontrolled! It is perfect for most of the user cases; now you can rewrite our suggestions example like this:
constructor(props) {
super(props);
this.handleOnChange = debounce(this.handleOnChange.bind(this), 100);
}
handleOnChange(value) {
this.setState({
value,
});
}
handleSuggestionSelect(value) {
this.setState({
value,
});
}
render() {
return (<div>
<LooseControlledInput
name="name"
type="text"
onChange={this.handleOnChange}
value={this.state.value} />;
<Suggestion query={this.state.value} onSelect={this.handleSuggestionSelect} />
</div>);
}
It will save you a lot of code to manipulate state for input values, and make your code much cleaner.
Let’s Build a Loose-Controlled Component
import React from 'react';
export default class LoosedControlledInput extends React.Component {
constructor(props) {
super(props);
this.state = {
value: this.props.value || this.props.defaultValue || '',
};
this.onHandleChange = this.onHandleChange.bind(this);
}
componentWillReceiveProps(nextProps) {
if (typeof nextProps.value !== 'undefined') {
this.setState({
value: nextProps.value,
});
}
}
onHandleChange(event) {
this.setState({
value: event.target.value,
});
this.props.onChange(event);
}
render() {
return (<input
{...this.props}
value={this.state.value}
onChange={this.onHandleChange} />);
}
}
LoosedControlledInput.propTypes = {
onChange: React.PropTypes.func,
value: React.PropTypes.string,
defaultValue: React.PropTypes.string,
};
LoosedControlledInput.defaultProps = {
onChange: () => {},
};
The implementation of a loose-controlled form input is quite easy, you just need to three things:
- Use
value
to fully control theinput
element you are going to wrap withthis.state.value
- Get default value for
this.state.value
fromthis.props.value
orthis.props.defaultValue
, this make this element can also be used just as a uncontrolled input. - update
this.state.value
incomponentWillReceiveProps
wheverthis.props.value
is presented. - update
this.state.value
whenever theinput
changes, this is the key to make the component