Forms
Forms play a special role among the other DOM elements in React, and work a little different, as they already have some sort of state. This state is not related to React.
The state of a text field results from the value entered, the state of a checkbox or a radio button results from being selected or not, and <select></select>
lists hold the state of one or more <option></option>
elements that are selected. React does not change any of these values. If you feel comfortable using the form state, you can keep using it without issues and nothing changes for the development of your components.
React calls these components uncontrolled components as React does not concern itself with the state management of these components. State handling is either completely independent of React or only in the direction of DOM form state to React state, but never the other way round. A form element does not know about updates in React state and will keep showing the same value or status (in the case of checkboxes, selects and radio buttons) as before.
Controlled components, in contrast, are deeply linked to React State. An update in the React state will have an effect on the value or the status of the form element and vice versa. While controlled components are harder to implement, they are "safer" to use as it is less likely that both states differ from each other.
Uncontrolled Components
Uncontrolled components can take two different forms. First, plain form elements can be rendered which are processed server-side and do not interact with React at any time. The form is completely static so to say. React does not intervene if this is what is desired, and allows the developer to freely choose an approach.
But uncontrolled components could also still interact with React, which is the second form of an uncontrolled component. This variation of an uncontrolled component writes changes of the form element into React state either to validate data in the background or to render the data in a different place. Changes that have been made to the state in React in different parts of the application do not directly influence the form fields.
An example for an uncontrolled component:
The user can enter their username into a simple text field. The uncontrolled
component is notified of a change via the onChange
event and can process the username further if necessary. As React only reacts passively and is simply notified of changes in the text field, we still refer to these types of components as uncontrolled components.
In most cases, it is sufficient to define these type of uncontrolled components if forms are not overly complex. However, we have to remember that the react state and DOM state are completely decoupled from each other and only work in one direction. Once the onChange
event has been triggered, the React state can update safely. However, the text field would not update if changes to values of the React state had been made elsewhere in the application (for example due to a response in an asynchronous request).
A form field is said to be controlled as soon as a value
attribute is set. From this point on, React expects the developer to synchronize the React state with the form field state. If we only want to set an initial value without converting the complete component into a controlled component, React allows us to define a defaultValue
attribute instead of the usual value
attribute, the equivalent being defaultChecked
for checkboxes and radio buttons. The element itself will stay uncontrolled but show an initial value or status.
Controlled components
In order to portray state changes within form fields, as well as transferring changes made by users in form fields into state, a controlled component is needed. React fully takes care of the state handling of these form elements. We transfer a value to the value
attribute which we receive from the state and also derive the changed value and pass it back to the state.
React state is seen as a single source of truth (or a similar state container like Redux). The only relevant value is the one that can be found in React state and the corresponding input in a form will constantly reflect this value in the state.
Let's take a look at an example to illustrate this better:
At first glance the Controlled
component does not look very different from the Uncontrolled
component. The defining difference that turns this component into a controlled
one rather than uncontrolled
lies in line 29. The value
attribute of this <input />
indicates to React that it should now control the form element and that changes to the input field should be reflected in the state. In order to pass changes to the React state, it is important to define the onChange
handler to keep the form field and React state in sync. Failing to do that, will result in input fields that do not update and is — perhaps unsurprisingly — a mistake made relatively often.
There are a few other things to consider. The value
attribute is only ever allowed to be a string but never undefined
or null
.
The select
elements that have the multiple
attribute are an exception to the rule. The value
attribute in this case needs to be an array (rather than a string).
Noticed anything? I spoke of a value
attribute for a <select>
field. But normally an <option>
is selected by setting its selected
attribute in HTML. React works a little different and controls the value with another value
attribute. The same applies to the <textarea>
element (which usually indicates its initial value with the textContent
attribute).
React unifies the mechanism for changing values by enforcing a value
attribute for the input
, textarea
and select
elements (with the exceptions of checkbox
and radio
inputs). This attribute always has to be a string or, in the case of a select
with a multiple
attribute, an array of strings.
Changes made to the form elements always need to be sent back to React state. This can become cumbersome, especially when dealing with checkboxes or radio buttons which do not only change a value but a status (checked
).
The following example of a controlled component should provide an exhaustive list of all basic types of HTML form elements. Any other input
elements not listed like email
, date
and range
work exactly the same.
The core of the form is formed by three event handlers that cater to the different types of form elements: changeValue
, changeCheckbox
and changeSelect
.
These are triggered by the onChange
events in their corresponding form elements and are passed an object of type SyntheticEvent
. We access properties of the target
property of the SyntheticEvent
via ES2015 object destructuring in order to update React state.
For elements of type <input type="text" />
, <input type="radio" />
and <textarea />
, we pick name
and value
, for <input type="checkbox" />
elements name
and the checked
property are important, whereas select
elements also need to provide a name
and whether a selection is offered to the user or a multiple select (with value
or selectedOptions
). We can find out whether we're dealing with a simple or multiple select by inspecting the multiple
property with e.target
.
Changing of values
If a value is modified, as is the case with text inputs and radio buttons, the corresponding React state is set to the value provided by the user, triggered by the onChange
event. Controlled components now mandate the following procedure:
When the user inputs text, the value changes.
The
onChange
event is triggered and processed by the event handler.The event handler sets state using the new value.
React re-renders the user interface and sets
this.state
to the new value.The user sees their newly provided value on the screen.
This is business as usual for the user and they will not notice that the form works differently behind the scenes and does not reflect usual browser behavior. React fully takes care of the logic in the background and painted a new "frame" in the user interface.
Changing state in checkboxes and radio buttons
Checkboxes (<input type="checkbox" />
) work in a similar fashion but their value will remain the same. Checkboxes change their state rather than their value by providing the boolean true
or false
in its checked
property. If the checked
property is controlled by React, the form field is said to be controlled. One can check whether the checkbox is activated (true
) or not (false
) by inspecting e.target.checked
in the event handler which passes this information to React state. React then takes care of the re-render and showing the status of the checkbox to the user.
Radio buttons on the other hand are a kind of a hybrid element. Similarly to checkboxes, radio buttons are seen as controlled if their checked
attribute is managed by React. However, there are often multiple radio buttons containing the same name but different values within the same document. It would not make sense to set the values of these names to either true
or false
as we are interested in the actual value of the selected radio button. Thus, the value of the radio button is written into state. We can check whether the selected value in the state is the same as the value of the field with checked={this.state.radio === "1"}
. We set checked
to true in this case if the value of the radio button radio
is equal to 1.
Changing state with simple or multiple selects
Let's start with a simple use case: a simple <select>
list modifies its value just like a text field, then triggers a re-render and finally shows the selected value in the freshly painted user interface. Multiple selects form an exception though.
Other than their counterparts, a simple select list or a simple text input, multiple select lists do not expect a string value but an array of strings. Annoyingly, we have to construct this array ourselves as e.target.value
only ever contains a single value, even if multiple options are possible. The e.target.selectedOptions
property, an object of type HTMLCollection
, can help us and contains a list of <option>
elements which are currently selected. This object can easily be transformed into an array using the static array method Array.from()
added in ES2015. Using Array.map()
we can furthermore iterate over this array and return a new array containing all the relevant values:
The newly created array is then written into state as a new value. But before that we check using e.target.multiple
whether we are actually dealing with a <select>
with multiple choices as it is only this <select>
which expects an array as a value.
Alternatively, we could have passed the changeValue
method to the simple select and the changeSelect
method to the select with multiple choices. Because each select would have received its own event handler, we could have avoided the additional check of checking for a multiple
select. However, following the procedure I have shown above will make your code more resilient to change requests as the type can be easily changed in the future. In the end it is up to you though.
Special cases within controlled components
I have used the name
attribute as the key in the above example to save their value in state. This can come in handy while working with server-side React and if forms are automatically generated and processed. It is not a requirement though: theoretically neither a name
attribute nor the exact match of the state name and the name
attribute is needed.
Saved values can also be nested and require to have more than one form within a component (But attention: React Anti-pattern). It is also not necessary to use React's own state to portray a controlled component. In practice, many developers choose to use an external state container such as Redux, Unstated or MobX.
Summary
Forms in React can take a controlled or uncontrolled form.
Uncontrolled components are usually sufficient for simple forms, however it is recommended to have React control your form components to ensure a single source of truth. To achieve this, the value
or checked
attribute needs to be controlled by React while the developer needs to manually react to changes.
In contrast to regular HTML forms, React expects the value of textareas, selects and inputs to be in a value
attribute.
Last updated