1. Uncontrolled Inputs
The most basic way of working with forms in React is to use what are referred to as “uncontrolled” form inputs. What this means is that React doesn’t track the input’s state. HTML input elements naturally keep track of their own state as part of the DOM, and so when the form is submitted we have to read the values from the DOM elements themselves.
In order to do this, React allows us to create a “ref” (reference) to associate with an element, giving access to the underlying DOM node. Let’s see how to do this:
class SimpleForm extends React.Component {
constructor(props) {
super(props);
// create a ref to store the DOM element
this.nameEl = React.createRef();
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(e) {
e.preventDefault();
alert(this.nameEl.current.value);
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>Name:
<input type="text" ref={this.nameEl} />
</label>
<input type="submit" name="Submit" />
</form>
)
}
}
As you can see above, for a class-based component you initialize a new ref in the constructor by calling React.createRef, assigning it to an instance property so it’s available for the lifetime of the component.
In order to associate the ref with an input, it’s passed to the element as the special ref attribute. Once this is done, the input’s underlying DOM node can be accessed via this.nameEl.current.
Let’s see how this looks in a functional component:
function SimpleForm(props) {
const nameEl = React.useRef(null);
const handleSubmit = e => {
e.preventDefault();
alert(nameEl.current.value);
};
return (
<form onSubmit={handleSubmit}>
<label>Name:
<input type="text" ref={nameEl} />
</label>
<input type="submit" name="Submit" />
</form>
);
}
There’s not a lot of difference here, other than swapping out createRef for the useRef hook.
Example: login form
function LoginForm(props) {
const nameEl = React.useRef(null);
const passwordEl = React.useRef(null);
const rememberMeEl = React.useRef(null);
const handleSubmit = e => {
e.preventDefault();
const data = {
username: nameEl.current.value,
password: passwordEl.current.value,
rememberMe: rememberMeEl.current.checked,
}
// Submit form details to login endpoint etc.
// ...
};
return (
<form onSubmit={handleSubmit}>
<input type="text" placeholder="username" ref={nameEl} />
<input type="password" placeholder="password" ref={passwordEl} />
<label>
<input type="checkbox" ref={rememberMeEl} />
Remember me
</label>
<button type="submit" className="myButton">Login</button>
</form>
);
}
While uncontrolled inputs work fine for quick and simple forms, they do have some drawbacks. As you might have noticed from the code above, we have to read the value from the input element whenever we want it. This means we can’t provide instant validation on the field as the user types, nor can we do things like enforce a custom input format, conditionally show or hide form elements, or disable/enable the submit button.
Fortunately, there’s a more sophisticated way to handle inputs in React.
2. Controlled Inputs
An input is said to be “controlled” when React is responsible for maintaining and setting its state. The state is kept in sync with the input’s value, meaning that changing the input will update the state, and updating the state will change the input.
Let’s see what that looks like with an example:
class ControlledInput extends React.Component {
constructor(props) {
super(props);
this.state = { name: '' };
this.handleInput = this.handleInput.bind(this);
}
handleInput(event) {
this.setState({
name: event.target.value
});
}
render() {
return (
<input type="text" value={this.state.name} onChange={this.handleInput} />
);
}
}
As you can see, we set up a kind of circular data flow: state to input value, on change event to state, and back again. This loop allows us a lot of control over the input, as we can react to changes to the value on the fly. Because of this, controlled inputs don’t suffer from the limitations of uncontrolled ones, opening up the follow possibilities:
instant input validation: we can give the user instant feedback without having to wait for them to submit the form (e.g. if their password is not complex enough)
instant input formatting: we can add proper separators to currency inputs, or grouping to phone numbers on the fly
conditionally disable form submission: we can enable the submit button after certain criteria are met (e.g. the user consented to the terms and conditions)
dynamically generate new inputs: we can add additional inputs to a form based on the user’s previous input (e.g. adding details of additional people on a hotel booking)
Validation
As I mentioned above, the continuous update loop of controlled components makes it possible to perform continuous validation on inputs as the user types. A handler attached to an input’s onChange event will be fired on every keystroke, allowing you to instantly validate or format the value.
Example: credit card validation
Let’s take a look at a real-word example of checking a credit card number as the user types it into a payment form.
The example uses a library called credit-card-type to determine the card issuer (such as Amex, Visa, or Mastercard) as the user types. The component then uses this information to display an image of the issuer logo next to the input:
import creditCardType from "credit-card-type";
function CreditCardForm(props) {
const [cardNumber, setCardNumber] = React.useState("");
const [cardTypeImage, setCardTypeImage] = React.useState(
"card-logo-unknown.svg"
);
const handleCardNumber = (e) => {
e.preventDefault();
const value = e.target.value;
setCardNumber(value);
let suggestion;
if (value.length > 0) {
suggestion = creditCardType(e.target.value)[0];
}
const cardType = suggestion ? suggestion.type : "unknown";
let imageUrl;
switch (cardType) {
case "visa":
imageUrl = "card-logo-visa.svg";
break;
case "mastercard":
imageUrl = "card-logo-mastercard.svg";
break;
case "american-express":
imageUrl = "card-logo-amex.svg";
break;
default:
imageUrl = "card-logo-unknown.svg";
}
setCardTypeImage(imageUrl);
};
return (
<form>
<div className="card-number">
<input
type="text"
placeholder="card number"
value={cardNumber}
onChange={handleCardNumber}
/>
<img src={cardTypeImage} alt="card logo" />
</div>
<button type="submit" className="myButton">
Login
</button>
</form>
);
}
The input’s onChange handler calls the creditCardType() function with the current value. This returns an array of matches (or an empty array) which can be used to determine which image to display. The image URL is then set to a state variable to be rendered into the form.