Bonus: Input Cheatsheet
(Optional lesson)
There are lots of different form controls on the web, and it can be hard to remember exactly which properties each one takes.
This lesson is an appendix that details how to use the most common form inputs.
I suggest bookmarking this lesson. Whenever you're building a form, I hope this lesson can jog your memory!
Text inputs
For controlled text inputs, we bind the React state to the value
attribute. We can set the initial value for uncontrolled text inputs with defaultValue
.
Here's how to use a controlled text input:
Code Playground
Gotchas
When working with controlled text inputs, be sure to use an empty string (''
) as the initial state. Otherwise, you risk running into edge-cases caused by the flip from uncontrolled to controlled.
// 🚫 Incorrect:const [email, setEmail] = React.useState();
// ✅ Correct:const [email, setEmail] = React.useState('');
For more information about why this is necessary, check out the “Data Binding” lesson.
Text input variants
In addition to plain text inputs, we can pick from different “formatted” text inputs, for things like email addresses, phone numbers, and passwords.
Here's the good news: These variants all work the same way, as far as data binding is concerned.
For example, here's how we'd bind a password
input:
const [secret, setSecret] = React.useState('');
<input type="password" value={secret} onChange={(event) => { setSecret(event.target.value); }}/>
In addition to text input variants, the <input>
tag can also shape-shift into entirely separate form controls. Later in this lesson, we'll talk about radio buttons, checkboxes, and specialty inputs like sliders and color pickers.
Textareas
In React, <textarea>
elements work exactly like text inputs. We set value
to bind it to React state, and defaultValue
to set an initial value for uncontrolled components.
Here's how to use a controlled textarea:
Code Playground
Gotchas
As with text inputs, be sure to use an empty string (''
) as the initial state. Otherwise, you risk running into edge-cases caused by the flip from uncontrolled to controlled.
// 🚫 Incorrect:const [email, setEmail] = React.useState();
// ✅ Correct:const [email, setEmail] = React.useState('');
Radio buttons
Things are a bit different when it comes to radio buttons. To wire up a radio button so that it's controlled by React, we need to set the checked
property to a boolean value. It specifies whether the radio button is currently ticked or not.
The defaultChecked
property can be used to set the initial value without making it a controlled input.
Minimal controlled example
Code Playground
When it comes to radio buttons, there are lots of attributes to keep in mind. Here's a table summarizing them:
Attribute | Type | Explanation |
---|---|---|
id | string | A globally-unique identifier for this radio button, used to improve accessibility and usability. |
name | string | Groups a set of radio buttons together, so that only one can be selected at a time. Must be the same value for all radio buttons in the group. |
value | string | Specifies the “thing” that this radio button represents. This is what will be captured/stored if this particular option is selected. |
checked | boolean | Controls whether the radio button is checked or not. By passing a boolean value, React will make this a “controlled” input. |
onChange | function | Like other form controls, this function will be invoked when the user changes the selected option. We use this function to update our state. |
Iterative controlled example
Because radio buttons have so many dang attributes, it often helps to generate them iteratively; that way, we only have to write the JSX once!
This is also required when the options themselves are dynamic (eg. fetched from the server).
Here's an example:
Code Playground
Gotchas
When using iteration to dynamically create radio buttons, we need to be careful not to accidentally “re-use” a variable name used by our state variable.
Avoid doing this:
const [language, setLanguage] = React.useState();
return VALID_LANGUAGES.map((language) => ( <input type="radio" name="current-language" id={language} value={language} checked={language === language} onChange={event => { setLanguage(event.target.value); }} />));
In our .map()
call, we're naming the map parameter language
, but that name is already taken! Our state variable is also called language
.
This is known as “shadowing”, and it essentially means that we've lost access to the outer language
value. This is a problem, because we need it to accurately set the checked
attribute!
For this reason, I like to use the generic option
name when iterating over possible options:
VALID_LANGUAGES.map(option => { <input type="radio" name="current-language" id={option} value={option} checked={option === language} onChange={event => { setLanguage(event.target.value); }} />})
Checkboxes
As with radio buttons, the checked
property is used to create a controlled element. It should be a boolean value, specifying whether the checkbox is currently ticked or not.
The defaultChecked
property can be used to set the initial value without making it a controlled input.
With checkboxes, the approach differs depending on whether we're working with a single checkbox or a group of checkboxes. Let's look at each in turn.
Single checkbox example
Code Playground
We store a boolean state variable, optIn
, and set it as the checked
value. When optIn
is true, the checkbox is ticked. Otherwise, the checkbox is unticked.
Things get trickier when we need to drive multiple checkboxes.
Multiple checkbox example
There are several ways to do this, but my favourite is to use a map-like object. Here's an example:
Code Playground
With radio buttons, we can fit everything we need to know into a single string: the value
of the selected option. But when we have a group of checkboxes, we need to store more data, since the user can select multiple options.
Here is how I choose to represent this state:
const initialToppings = { anchovies: false, chicken: false, tomatoes: false,}
In the JSX, we map over the keys from this object, and render a checkbox for each one. In the iteration, we look up whether this particular option is selected, and use it to control the checkbox with the checked
attribute.
We also pass a function to onChange
that will flip the value of the checkbox in question. Because React state needs to be immutable, we solve this by creating a near-identical new object, with the option in question flipped between true/false. I'm doing this with the help of the “spread” syntax 👀.
Here's a table showing each attribute's purpose:
Attribute | Type | Explanation |
---|---|---|
id | string | A globally-unique identifier for this checkbox, used to improve accessibility and usability. |
value | string | Specifies the “thing” that we're ticking off and on with this checkbox. |
checked | boolean | Controls whether the checkbox is checked or not. |
onChange | function | Like other form controls, this function will be invoked when the user ticks or unticks the checkbox. We use this function to update our state. |
(We can also specify a name
, as with radio buttons, though this isn't strictly necessary when working with controlled inputs.)
It may help to revisit the lessons on complex state.
Select
To create a controlled select tag, we use the value
attribute. We update the value with an onChange
handler. In effect, it works exactly like a text input!
For uncontrolled select tags, the initial value can be set with defaultValue
.
Here's how to use a controlled select:
Code Playground
Perhaps more than any other tag, <select>
has been modified for React. In a vanilla HTML/JS context, you'd need to reach down and toggle the selected
attribute on the appropriate <option>
child. Fortunately, this is not required when working with controlled select tags in React.
Gotchas
As with text inputs, we need to initialize the state to a valid value. This means that our state variable's initial value must match one of the options:
// This initial value:const [age, setAge] = React.useState("0-18");
// Must match one of the options:<select> <option value="0-18" > 18 and under </option></select>
This is a smelly fish. One small typo, and we risk running into some very confusing bugs.
To avoid this potential footgun, I prefer to generate the <option>
tags dynamically, using a single source of truth:
Code Playground
Specialty inputs
As we've seen, the <input>
HTML tag can take many different forms. Depending on the type
attribute, it can be a text input, a password input, a checkbox, a radio button…
In fact, MDN lists 22 different valid values for the type
attribute. Some of these are “special”, and have a unique appearance:
- Sliders (with
type="range"
) - Date pickers (with
type="date"
) - Color pickers (with
type="color"
)
Fortunately, they all follow the same pattern as text inputs. We use value
to lock the input to the state's value, and onChange
to update that value when the input is edited.
Here's an example using <input type="range">
:
Code Playground
Here's another example, with <input type="color">
:
Code Playground
Gotchas
As with text inputs, we don't want to flip from uncontrolled to controlled. We need to initialize our state variable to a valid value.
For example, for range inputs, this would be a number:
// 🚫 Incorrect:const [value, setValue] = React.useState();
// ✅ Correct:const [value, setValue] = React.useState(5);