(Part 2) Build quality forms with React ๐
Optimize developer experience, ship with confidence
Photo by Kelly Sikkema
This article is the second and final part of "Build quality forms with React ๐". In the first part, I explained what controlled and uncontrolled forms are in React, and I introduced two wonderful libraries: react-hook-form and yup.
Putting it all together
To demonstrate how it all ties up together, I will walk you through the process of building a simple yet solid login form. It will be made of two inputs (email and password), a checkbox "remember me" to keep the user session alive, and a submit button. We will also add a link at the bottom of the form, to switch to the registration form that I will also show you later.
We won't be implementing any real authentication, the goal here is to focus only on the form and its validation. However, I will use Bootstrap class utilities for styling, so you get a good idea of what a "real-life" form would look like code-wise. Here is what the form will look like:
And here is what happens if we hit the login button without filling out the email nor the password:
First, we start by declaring the Yup schema:
const schema = yup.object().shape({
email: yup
.string()
.email('This is not a valid email address')
.required('Please fill in your email'),
password: yup.string().required('Please fill in your password'),
rememberMe: yup.boolean(),
})
Pretty simple, as you can see. It works by chaining validators for a specific field. For example, an email should be:
- a string:
.string()
- also it should look like an email address:
.email()
- and it is required:
.required()
You can specify an error message for each validation type, which is what I did, in order to provide accurate messages depending on the failure reason.
Once this is done, the next step is to type our form values:
// Type definition for the form data
type FormValues = {
email: string
password: string
rememberMe: boolean
}
We will provide this type to react-hook-form, which will provide us a type-safe developer experience later when registering our inputs.
Speaking of react-hook-form, it's now time to start writing our login form component. We start by initializing the form using the useForm
hook:
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({ resolver: yupResolver(schema) })
As you can see, we destructure the return of useForm
to get what we need. In our case, we only need a few things:
register
to register our inputs laterhandleSubmit
to wrap our submit handler (so that react-hook-form can trigger form validation before calling it)formState
: we can get many state values out of this, but here we only are interested in our form errors. If a validation error occurs for a specific field when submitting, the error message will be stored inerrors.email.message
for example
We type useForm
with our FormValues
to tell react-hook-form about the structure of our form data, and finally, we specify one option, which is the resolver Yup. This is where it all comes together: react-hook-form will now use the yup validation schema we defined before to validate our form. For example, if the user types something that is not a valid email address in the email input and tries to submit the form, errors.email.message
will be "Please fill in your email".
Let's write a simple submit handler which will just log the form data to the console:
const onSubmit: SubmitHandler<FormValues> = (data) => console.log(data)
Thanks to TypeScript, data
is strongly typed so we don't make mistakes while accessing and manipulating it:
Now that everything is set up, let's render our form! As mentioned before, I'm using Bootstrap utility classes for styling purposes, just to demonstrate the fact that react-hook-form leaves the styling completely up to you. I won't explain what those classes do here, but you check their documentation if you're curious. Anyway, here is the final version of our Login form:
// LoginForm.tsx
import { SubmitHandler, useForm } from 'react-hook-form'
import { Link } from 'react-router-dom'
import * as yup from 'yup'
import { yupResolver } from '@hookform/resolvers/yup'
import cn from 'classnames'
// Yup validation schema
const schema = yup.object().shape({
email: yup
.string()
.email('This is not a valid email address')
.required('Please fill in your email'),
password: yup.string().required('Please fill in your password'),
rememberMe: yup.boolean(),
})
// Type definition for the form data
type FormValues = {
email: string
password: string
rememberMe: boolean
}
/**
* Simple Login Form Component
*/
export default function LoginForm() {
// Initiate the form using react-hook-form's hook
// We get some utilities & state in return
const {
register, // you need this to register your inputs
handleSubmit, // this is a wrapper for your onSubmit handler
formState: { errors }, // we get our form's state, especially the errors
} = useForm<FormValues>({ resolver: yupResolver(schema) })
const onSubmit: SubmitHandler<FormValues> = (data) => console.log(data)
return (
/**
* A few things to note here:
* - the onSubmit callback needs to be wrapped with react-hook-form's
* handleSubmit utility
* - the "noValidate" prop on the form is here because we manage
* validation ourselves, with Yup, so we don't want the browser validation
**/
<form onSubmit={handleSubmit(onSubmit)} noValidate>
{/** Email */}
<div className="mb-3">
<label htmlFor="email" className="form-label">
Email address
</label>
<input
id="email"
type="email"
className={cn('form-control', {
/**
* is-invalid is a Bootstrap utility class to highligh the
* input in red in case of an error. If errors.email is not
* nullish, it means there were a validation error regarding
* this field
*/
'is-invalid': Boolean(errors.email),
})}
{...register('email')}
/>
{/** Render the error message related to this field (empty if no error) */}
<div className="invalid-feedback">{errors.email?.message}</div>
</div>
{/** Password */}
<div className="mb-3">
<label htmlFor="password" className="form-label">
Password
</label>
<input
id="password"
type="password"
className={cn('form-control', {
'is-invalid': Boolean(errors.password),
})}
{...register('password')}
/>
<div className="invalid-feedback">{errors.password?.message}</div>
</div>
{/** Remember me */}
<div className="mb-3 form-check">
<input
id="rememberMe"
type="checkbox"
className="form-check-input"
{...register('rememberMe')}
/>
<label className="form-check-label" htmlFor="rememberMe">
Remember me
</label>
</div>
{/** Submit button */}
<button type="submit" className="btn btn-primary w-100 mb-3">
Login
</button>
<Link to="/register">You're new ? Register here !</Link>
</form>
)
}
Here we go, pretty powerful, right? The entire validation process and the state management are being handled by react-hook-form and yup so we can focus on making a beautiful and accessible form.
Earlier, I promised you a registration form, and I'm a man of my word, so here you go. Also, this allows me to demonstrate a few more things:
- A few more yup validation rules, like
oneOf
which is used to easily validate that the "Confirm password" value is the same as "Password", or to make a boolean required (without this, the value "false" would pass the required rule, because it's not nullish) - The integration of a more complex UI component with
react-select
: this is done using react-hook-form's Controller, which is amazingly simple. You provide it acontrol
prop (retrieved from the hook) and arender
prop to render your custom component using the same benefits as withregister
- The use of UI components instead of HTML markup, just to once again demonstrate that react-hook-form leaves the styling completely up to you
The form will look like this:
Here is the code:
// RegisterForm.tsx
import { useForm, Controller } from 'react-hook-form'
import { Link } from 'react-router-dom'
import Select, { OptionTypeBase } from 'react-select'
import * as yup from 'yup'
import { yupResolver } from '@hookform/resolvers/yup'
// For this one, we will use react-boostrap component instead of HTML markup
// for the form (because why not ? it's up to us ๐)
import Form from 'react-bootstrap/Form'
import Button from 'react-bootstrap/Button'
// Options for react-select
// We use this to ask the user about is favorite ice cream ๐ฆ
const iceCreamOptions: OptionTypeBase[] = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' },
]
// Yup validation schema
const schema = yup.object().shape({
email: yup
.string()
.email('This is not a valid email address')
.required('Please fill in your email'),
password: yup.string().required('Please fill in your password'),
passwordConfirm: yup
.string()
// Simple trick to validate that passwordConfirm is the same as passwor
.oneOf([yup.ref('password')])
.required('Passwords must match'),
// favoriteIceCream isn't required, so we require its value to be one of the options, or null
favoriteIceCream: yup.string().oneOf([...iceCreamOptions.map((option) => option.value), null]),
acceptLegal: yup.boolean().oneOf([true], 'You must accept the Terms & Conditions'),
})
// Type definition for the form data
type FormValues = {
email: string
password: string
passwordConfirm: string
favoriteIceCream: OptionTypeBase
acceptLegal: boolean
}
/**
* Registration form
*/
export default function RegisterForm() {
const {
register,
handleSubmit,
formState: { errors },
control,
} = useForm<FormValues>({ resolver: yupResolver(schema) })
function onSubmit(data: FormValues) {
// When this is being executed, you know for sure that the form data is valid
// according to the yup validation schema defined above
const { passwordConfirm, acceptLegal, ...rest } = data
console.log(rest) // you would only send what's in rest to your backend
}
return (
<Form onSubmit={handleSubmit(onSubmit)} noValidate>
{/** Email */}
<Form.Group controlId="email" className="mb-3">
<Form.Label>Email address</Form.Label>
<Form.Control type="email" isInvalid={Boolean(errors.email)} {...register('email')} />
<Form.Text className="invalid-feedback">{errors.email?.message}</Form.Text>
</Form.Group>
{/** Password */}
<Form.Group controlId="password" className="mb-3">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
isInvalid={Boolean(errors.password)}
{...register('password')}
/>
<Form.Text className="invalid-feedback">{errors.password?.message}</Form.Text>
</Form.Group>
{/** Password Confirmation */}
<Form.Group controlId="passwordConfirm" className="mb-3">
<Form.Label>Confirm password</Form.Label>
<Form.Control
type="password"
isInvalid={Boolean(errors.passwordConfirm)}
{...register('passwordConfirm')}
/>
<Form.Text className="invalid-feedback">{errors.passwordConfirm?.message}</Form.Text>
</Form.Group>
{/* Favorite Ice Cream */}
<Form.Group controlId="favoriteIceCream" className="mb-3">
<Form.Label>Favorite ice cream</Form.Label>
<Controller
name="favoriteIceCream" // equivalent to register('favoriteIceCream')
control={control}
render={({ field }) => <Select {...field} options={iceCreamOptions} />}
/>
</Form.Group>
{/** Accept terms & conditions */}
<Form.Group controlId="acceptLegal" className="mb-3">
<Form.Check
type="checkbox"
isInvalid={Boolean(errors.acceptLegal)}
feedback={errors.acceptLegal?.message}
label={
<>
I accept the <a href="#">Terms & Conditions</a>
</>
}
{...register('acceptLegal')}
/>
</Form.Group>
{/** Submit button */}
<Button variant="primary" type="submit" className="w-100 mb-3">
Register
</Button>
<Link to="/">Already have an account ? Login</Link>
</Form>
)
}
Conclusion
I hope that you enjoyed this humble guide to building quality forms with React, using two wonderful libraries: react-hook-form and yup. This is what I use every time I need to build a form, and I feel really happy and confident about the process. The fact that I can focus on the UI, features, and accessibility without worrying too much about state management or validation myself is absolutely priceless, and it saves me a lot of time.
Check out the repository with all 4 forms (Controlled, Uncontrolled, Login and Register) with a fully functional user navigation using react-router, or open it in a code sandbox (read my post on CodeSandbox & Github Magic to learn more about this kind of magic links ๐ง)
Feel free to let me know what your thoughts are on the subject in the comments, and share this post with your peers if you liked it!