Sooner or later every React developer has to handle forms. The following tutorial will give you a comprehensive overview about forms in React.
You will learn how to manage form state in React, the difference of controlled and uncontrolled forms (state vs reference), how to submit a form (e.g. callback handler), and how to reset a form (e.g. after submit). In addition you will learn about advanced topics such as dirty fields and validation in React forms.
While learning how to implement these advanced topics in React without any form library, you will get to know how form libraries would perform these tasks for you. Eventually you will use such form library yourself, for example React Hook Form, to accomplish these more advanced tasks for you.
Table of Contents
- React Form by Example
- React Form with onSubmit
- Uncontrolled React Form
- Controlled React Form
- Controlled vs Uncontrolled Forms
- Submit a React Form
- React Form Reset
- React Form Template
- React Form Dirty
- React Form with Validation
- Form Library: React Hook Form
React Form by Example
A common example of a form in a web application these days is a login form. It allows the authentication and authorization of a user to access the application. In React, we would use a functional component to represent such form:
import * as React from 'react';const LoginForm = () => {return (<form><div><label htmlFor="email">Email</label><input id="email" type="text" /></div><div><label htmlFor="password">Password</label><input id="password" type="password" /></div><button>Submit</button></form>);};export { LoginForm };
The form component displays two HTML input fields with each having an attached HTML label element. All elements are used within a HTML form element.
For accessibility reasons in a form, a HTML label element can use a htmlFor
attribute (React specific) which links to the HTML input element's id
attribute. When clicking the form's labels, the respective input fields should get focused.
Note that the form does not receive any props yet. Later it could receive props for its initial state which populate the form or a callback handler which gets executed when clicking the form's submit button.
React Form with onSubmit
When a user clicks the submit button of a form, we can use the HTML form element's onSubmit attribute for attaching an event handler to it. In order to tell the form that the button should initiate the form's event handler, the button has to have the submit type:
import * as React from 'react';const LoginForm = () => {const handleSubmit = (event) => {event.preventDefault();};return (<form onSubmit={handleSubmit}><div><label htmlFor="email">Email</label><input id="email" type="text" /></div><div><label htmlFor="password">Password</label><input id="password" type="password" /></div><button type="submit">Submit</button></form>);};export { LoginForm };
For preventing the native browser behavior (which would perform a refresh on the browser), we can use the preventDefault()
method on the form's event.
Uncontrolled React Form
When submitting a form, we want to read the values from the form. In React we can get access to HTML elements by attaching references to them. So whenever we want to access a HTML element in JSX, we would be using React's useRef Hook:
import * as React from 'react';const LoginForm = () => {const emailRef = React.useRef();const passwordRef = React.useRef();const handleSubmit = (event) => {event.preventDefault();const email = emailRef.current.valueconst password = passwordRef.current.valuealert(email + ' ' + password);};return (<form onSubmit={handleSubmit}><div><label htmlFor="email">Email</label><input id="email" type="text" ref={emailRef} /></div><div><label htmlFor="password">Password</label><input id="password" type="password" ref={passwordRef} /></div><button type="submit">Submit</button></form>);};export { LoginForm };
Attaching a ref to each form field may be too much hassle when taking the uncontrolled form approach. The lazy approach would be reading the form values directly from the form's event, because the form knows its elements and respective values:
import * as React from 'react';const LoginForm = () => {const handleSubmit = (event) => {event.preventDefault();const email = event.target.elements.email.value;const password = event.target.elements.password.value;alert(email + ' ' + password);};return (<form onSubmit={handleSubmit}><div><label htmlFor="email">Email</label><input id="email" type="text" /></div><div><label htmlFor="password">Password</label><input id="password" type="password" /></div><button type="submit">Submit</button></form>);};export { LoginForm };
If we have a straightforward form where we do not need to fiddle with the form state, we could go with the uncontrolled form approach. However, the more idiomatic React way would be using controlled forms.
Controlled React Form
The idiomatic way of using forms in React would be using React's declarative nature. We would use React's useState Hook to manage the form state ourselves. By updating this state with each input field's onChange
handler, we can use the state (here: email
and password
) respectively by passing it to each input field. This way, each input field gets controlled by React and does not manage its internal native HTML state anymore:
import * as React from 'react';const LoginForm = () => {const [email, setEmail] = React.useState('');const [password, setPassword] = React.useState('');const handleEmail = (event) => {setEmail(event.target.value);};const handlePassword = (event) => {setPassword(event.target.value);};const handleSubmit = (event) => {event.preventDefault();alert(email + ' ' + password);};return (<form onSubmit={handleSubmit}><div><label htmlFor="email">Email</label><inputid="email"type="text"value={email}onChange={handleEmail}/></div><div><label htmlFor="password">Password</label><inputid="password"type="password"value={password}onChange={handlePassword}/></div><button type="submit">Submit</button></form>);};export { LoginForm };
Once the form grows gets bigger, you will get to a point where it has too many handlers for managing the state of each form field. Then you can use the following strategy:
import * as React from 'react';const LoginForm = () => {const [form, setForm] = React.useState({email: '',password: '',});const handleChange = (event) => {setForm({...form,[event.target.id]: event.target.value,});};const handleSubmit = (event) => {event.preventDefault();alert(form.email + ' ' + form.password);};return (<form onSubmit={handleSubmit}><div><label htmlFor="email">Email</label><inputid="email"type="text"value={form.email}onChange={handleChange}/></div><div><label htmlFor="password">Password</label><inputid="password"type="password"value={form.password}onChange={handleChange}/></div><button type="submit">Submit</button></form>);};export { LoginForm };
The strategy unifies all the form state into one object and all event handlers into one handler. By leveraging each form field's identifier, we can use it in the unified handler to update the state by using the identifier as dynamic key.
This scales a controlled form in React well, because state, handler, and form field are not in a 1:1:1 relationship anymore. In contrast, each handler can reuse the state and handler.
Controlled vs Uncontrolled Forms
In practice there is not much discussion going on about uncontrolled vs controlled forms in React. If the form is simple, one can go with an uncontrolled form. However, once the form gets more requirements (e.g. having control over the state), you would have to use a controlled form.
The following form example illustrates how to reset a form after a submit operation:
const LoginForm = () => {const [form, setForm] = React.useState({email: 'john@doe.com',password: 'geheim',});const handleChange = (event) => {setForm({...form,[event.target.id]: event.target.value,});};const handleSubmit = (event) => {event.preventDefault();setForm({email: '',password: '',});};return (...);};
While controlled forms are more popular in React, because they allow you a better developer experience for managing the form's state (e.g. initial state, updating state), they are more performance intensive. Each change of the state comes with a re-render of the form. For an uncontrolled form, there are no re-renders. Anyway, most of the time this performance impact isn't perceived by any user.
Submit a React Form
You have already seen how to create a submit button for a form in React. So far, we only triggered this button and used its attached event handler, but we didn't send any form data yet. Usually a form component receives a callback handler from a parent component which uses the form data:
const LoginForm = ({ onLogin }) => {const [form, setForm] = React.useState({email: '',password: '',});const handleChange = (event) => {setForm({...form,[event.target.id]: event.target.value,});};const handleSubmit = (event) => {event.preventDefault();onLogin(form);};return (...);};
The example shows how form state is passed to the callback handler as form data. Therefore, once a user clicks the submit button, the parent component will receive the form data and perform a task with it (e.g. post form data to a backend).
React Form Reset
Previously you have already seen a form reset example. However, in the previous example we reset each form field in the form state one by one (e.g. email and password). However, if we would extract the form state from the beginning as initial state to get started in the first place, we could reuse this initial state for the reset:
const INITIAL_STATE = {email: '',password: '',};const LoginForm = ({ onLogin }) => {const [form, setForm] = React.useState(INITIAL_STATE);...const handleSubmit = (event) => {event.preventDefault();// call your component's callback handler, e.g. onLoginsetForm(INITIAL_STATE);};return (...);};
Extracting the initial form state as variable often makes sense when dealing with forms in React. The reset is only one valuable example where this approach comes to fruition.
React Form Template
The previous examples have given you many copy and paste templates which get you started with a form in React. However, so far we have only used two HTML input elements in a React form. There are many other form fields that you could add as reusable component:
You have seen the basic usage of forms in React. Next we will walk through some more advanced form concepts which should illustrate the complexity of forms. While you walk through them, you learn how to implement these advanced concepts, however, note that eventually a dedicated form library will take care of these implementations.
React Form Dirty
A form is dirty if one of its form fields has been changed by a user. When using forms, the dirty state helps with certain scenarios. For example, the submit button should only be enabled if a form field has changed:
const INITIAL_STATE = {email: '',password: '',};const getDirtyFields = (form) =>Object.keys(form).reduce((acc, key) => {// check all form fields that have changedconst isDirty = form[key] !== INITIAL_STATE[key];return { ...acc, [key]: isDirty };}, {});const LoginForm = ({ onLogin }) => {const [form, setForm] = React.useState(INITIAL_STATE);...const dirtyFields = getDirtyFields(form);const hasChanges = Object.values(dirtyFields).every((isDirty) => !isDirty);return (<form onSubmit={handleSubmit}>...<button disabled={hasChanges} type="submit">Submit</button></form>);};
The previous code snippet shows an implementation of establishing a computed state which knows about each form field's dirty state. However, this already shows the complexity of managing this dirty state yourself. Hence my recommendation of using a form library like React Hook Form.
React Form with Validation
The most common culprit for using a form library is the validation of forms. While the following implementation seems rather straightforward, there are lots of moving parts which go into a sophisticated validation. So stay with me and learn how to perform such task yourself, but don't hesitate to use a form library for it eventually:
const INITIAL_STATE = {email: '',password: '',};const VALIDATION = {email: [{isValid: (value) => !!value,message: 'Is required.',},{isValid: (value) => /\S+@\S+\.\S+/.test(value),message: 'Needs to be an email.',},],password: [{isValid: (value) => !!value,message: 'Is required.',},],};const getErrorFields = (form) =>Object.keys(form).reduce((acc, key) => {if (!VALIDATION[key]) return acc;const errorsPerField = VALIDATION[key]// get a list of potential errors for each field// by running through all the checks.map((validation) => ({isValid: validation.isValid(form[key]),message: validation.message,}))// only keep the errors.filter((errorPerField) => !errorPerField.isValid);return { ...acc, [key]: errorsPerField };}, {});const LoginForm = ({ onLogin }) => {const [form, setForm] = React.useState(INITIAL_STATE);...const errorFields = getErrorFields(form);console.log(errorFields);return (...);};
By having all the errors per form field as computed properties, we can perform tasks like preventing a user from submitting the form if there is a validation error:
const LoginForm = ({ onLogin }) => {...const handleSubmit = (event) => {event.preventDefault();const hasErrors = Object.values(errorFields).flat().length > 0;if (hasErrors) return;// call your component's callback handler, e.g. onLogin};return (...);};
What may be almost more important is showing the user feedback about form errors. Because we have all the errors there, we can display them conditionally as hints in JSX:
const LoginForm = () => {...const errorFields = getErrorFields(form);return (<form onSubmit={handleSubmit}><div><label htmlFor="email">Email</label><inputid="email"type="text"value={form.email}onChange={handleChange}/>{errorFields.email?.length ? (<span style={{ color: 'red' }}>{errorFields.email[0].message}</span>) : null}</div><div><label htmlFor="password">Password</label><inputid="password"type="password"value={form.password}onChange={handleChange}/>{errorFields.password?.length ? (<span style={{ color: 'red' }}>{errorFields.password[0].message}</span>) : null}</div><button type="submit">Submit</button></form>);};
Learning more and more about form handling in React reveals how complex certain topics become over time. We only touched the surface here. It's great to learn how everything works under the hood, hence this tutorial about forms in React, however, eventually you should opt-in a form library like React Hook Form.
Form Library: React Hook Form
My go-to form library these days in React Hook Form. One could say that it is a headless form library, because it doesn't come with any form components, but just with custom React Hooks which allow you to access form state, dirty state, and validation state. But there is much more: Integration in third-party UI libraries, performant re-renders, form watchers, and usage of third-party validation libraries like Yup and Zod.