When building forms you have to make sure all fields are filled correctly. There are multiple solutions for this, aside to the basic HTML form validation. One of these options is a library called Yup. This tutorial will show you how to use Yup to create custom form validation for forms build with React.
You can find demo for this tutorial on my Codesandbox.
A word on form validation
There are multiple ways to solve the problem with form validation. The most basic and also most accessible is the native way. This is the validation provided by browsers. This validation works well if you use correct field types and don't need any customization. Then, there are bigger, all-in-one solutions, such as Formik.
These solutions offer a lot of flexibility and customization. They are often also more developer-friendly. The downside is that they are also heavier, or bigger, and often require deep implementation. If all you need is just one thing, such as validation, it may not be a reason to rewrite your solution to some framework.
The fact is, you don't have to do that. There is also the third option. There are libraries focused on helping with just one thing, such as form validation, and won't interfere with other things. One of these libraries is Yup. This library helps with validation of any kind, including forms.
Validation with Yup
The way Yups works is simple. You start by defining a schema. This is an object that specifies all values you want to check. It also specifies characteristics of each of these values. For example, you can define that you want to check a value for an email address. In schema, you can call this value email
. This will be the key on the schema object.
Next, you can specify that this email
value must be a string. Besides that, you can also specify that it should be "type" of an email
. This means that Yup will, as part of validation, test that string if it is in an actual email format. Since you may really need that email, for whatever reason, you can also specify that it is required.
There are many other options. You can also specify that something is a URL, or that the value can contain only numbers, or that it has to contain at least eight characters. Whatever custom validation rule you need chances are Yup will be able to help you.
A quick introduction
This tutorial will show you two things. First, it will show you how to create a simple form in React. Second, it will show you how to use Yup library to put together custom validation for custom React form. The form we will build will use useState hook for state management. We will also use memo and useCallback hooks.
A note on dependencies
This tutorial uses the create-react-app as the starting template. The react
and react-dom
dependencies are both version 17.0.2
. The react-scripts
is version 4.0.0
. The yup
library is the fourth dependency and it is version 0.32.9
. The fifth and last dependency is immutability-helper, version 3.1.1
.
This dependency helps to mutate a copy of data without changing the original source. You will use this dependency to update form states for values and errors. That's it. Now, let's get to the tutorial.
The form field component
The first component we need to build is a form field. This will be a simple component. It will render fieldset that will contain label and input, and simple error message. Each field will receive some data through props: onFieldChange
, labelText
, fieldType
, fieldName
, fieldValue
and hasError
.
The onFieldChange
is handler for input change event. To avoid using arrow function in render, we will create new handler for change event in the field component. This handler will call the onFieldChange
function passed through props with the fieldName
of the current field component and onChange event from the input passed as arguments.
Next, it will use the labelText
to render custom input label and the hasError
to show error message when appropriate. The fieldType
will specify the type of input we want to render. The fieldName
will specify the name
and id
attributes and help us pair the input with values and errors state. The fieldValue
will pass the input value.
// Import memo and useCallback hooks:
import { memo, useCallback } from 'react'
// Create the Field component:
export const Field = memo((props) => {
// Create handler for change event:
const onFieldChange = useCallback(
(event) => {
props.onFieldChange(props.fieldName, event.target.value)
},
[props.onFieldChange, props.fieldName]
)
// Render all HTML components:
return (
<fieldset>
<label htmlFor={props.fieldName}>{props.labelText}</label>
<input
type={props.fieldType}
name={props.fieldName}
id={props.fieldName}
onChange={onFieldChange}
value={props.fieldValue}
/>
{props.hasError && (
<p>{`Please fill in correct value for "${props.labelText}".`}</p>
)}
</fieldset>
)
})
The form component
The form component will be just a wrapper that renders individual <Field />
components. It will accept values
and errors
states (objects) and onSubmit
handler through props. Properties of values
and errors
states will be appropriately spread between individual <Field />
components.
// Import memo hook:
import { memo } from 'react'
// Import Field component:
import { Field } from './form-field'
// Create the Field component:
export const Form = memo((props) => (
<form onSubmit={props.onSubmit} noValidate>
<Field
labelText="First name"
fieldType="text"
fieldName="firstName"
fieldValue={props.values.firstName}
hasError={props.errors.firstName}
onFieldChange={props.onFieldChange}
/>
<Field
labelText="Last name"
fieldType="text"
fieldName="lastName"
fieldValue={props.values.lastName}
hasError={props.errors.lastName}
onFieldChange={props.onFieldChange}
/>
<Field
labelText="Email"
fieldType="email"
fieldName="email"
fieldValue={props.values.email}
hasError={props.errors.email}
onFieldChange={props.onFieldChange}
/>
<Field
labelText="Password (+8 characters)"
fieldType="password"
fieldName="password"
fieldValue={props.values.password}
hasError={props.errors.password}
onFieldChange={props.onFieldChange}
/>
<Field
labelText="Personal website"
fieldType="url"
fieldName="website"
fieldValue={props.values.website}
hasError={props.errors.website}
onFieldChange={props.onFieldChange}
/>
<button type="submit">Send</button>
</form>
))
The App component
The App
component will be the most complex. It will contain all the logic for the form.
The schema
First, we will create new schema object with Yup
. This schema will define all values (form fields) we want to validate. These values will be firstName
, lastName
, email
, password
and website
. We will want all these values to be string()
and required()
. We will specify the email
value to match email format, with email()
.
We will also specify that password
has to be at least 8 characters long with min(8)
. Lastly, we will specify that the website
has match URL format, with url()
.
// Create validation schema:
const formSchema = yup.object().shape({
firstName: yup.string().required(),
lastName: yup.string().required(),
email: yup.string().email().required(),
password: yup.string().min(8).required(),
website: yup.string().url().required(),
})
States
The App
component will contain two states, one for form values and one for form errors. Both states will be objects with keys that match keys in formSchema
and fieldName
property on <Field />
components. Initial values for form values will be empty strings. Initial values for form errors will be false
.
// ... previous code
export const App = memo(() => {
// Create state for form values:
const [values, setValues] = useState({
firstName: '',
lastName: '',
email: '',
password: '',
website: '',
})
// Create state for form errors:
const [errors, setErrors] = useState({
firstName: false,
lastName: false,
email: false,
password: false,
website: false,
})
// ... rest of the code
}
Input field change event handler
The App
component will also define the change handler function that will be passed through the <Form />
component to individual <Field />
components. This handler will use setter method for values
state and update
method from immutability-helper
to update value of a specific state key (field name).
The function will accept both, key (field name) and value to save in the state, as parameters. The immutability-helper
will ensure we are not updating any value directly and working with copies, not originals.
export const App = memo(() => {
// ... previous code
// Create handler for input change event:
const onFieldChange = useCallback((fieldName, value) => {
setValues((prevValues) =>
update(prevValues, {
[fieldName]: {
$set: value,
},
})
)
}, [])
// ... rest of the code
Form submit event handler
The Yup
library works with promises. This means that we can either use then() handler methods or async/await syntax to work with validation results. For now, we will use the async/await
syntax to avoid unnecessary nesting with then()
methods. First, we will declare the onSubmit
function as async
.
This will allow us to use the await
keyword inside this function when we will work with promises. The first thing the onSubmit
function will do is to prevent form submission with event.preventDefault()
. Next, we will check if form is valid by calling isValid()
method on schema for our form, assigned to formSchema
variable.
We will pass two arguments to the isValid()
method. The first will be the values state, object with all form fields and corresponding values. Second will be options object where we will set the abortEarly
option to false
. This means that if Yup encounters any error during validation it will not stop the process.
It will stop only after all form values are validated. Only then will it return the status for all specified values (form fields). Without this option, Yup would stop after first error and return only that. So, if there were multiple fields with errors, we would not know about it. We would know only about the first error Yup found.
We will assign the call to isValid()
method to a variable. This method returns a promise. So, we will use the await
keyword to pause the execution and wait for the promise to resolve and return some value. Next, we will check if the resolved value is true
. If it is, it means the form is valid. You can do whatever you need to submit the values.
If the resolved value is false
it can mean one of two things. It can mean that some value is missing, that some field is empty. Or it can mean that some value is in a wrong format. For example, password contains less than 8 characters or URL is not in a correct format. Thing is, we need to know what field has some error.
In order to get these errors we will call validate()
method on the schema object assigned to formSchema
variable. We will pass the same two arguments as to the isValid()
method: values state object and abortEarly
set to false
. This method also returns a promise. However, this promise doesn't resolve with errors, but rejects.
This means we will need the catch() handler function to get those errors. The error object returned by the promise contains property inner
. The value of this property is an array with all errors and details about them. We don't need all those details. We need just the name of the field so we know for which field we should show an error.
To reduce the amount of information for every error we will use the reduce() method. We will reduce each error object to a simple object where field name will be the key and true
will be its value. After this, we will use the immutability-helper
to update the errors
state.
export const App = memo(() => {
// ... previous code
// Create handler for form submit event:
const onSubmit = useCallback(
async (event) => {
// Prevent form from submitting:
event.preventDefault()
// Check the schema if form is valid:
const isFormValid = await formSchema.isValid(values, {
abortEarly: false, // Prevent aborting validation after first error
})
if (isFormValid) {
// If form is valid, continue submission.
console.log('Form is legit')
} else {
// If form is not valid, check which fields are incorrect:
formSchema.validate(values, { abortEarly: false }).catch((err) => {
// Collect all errors in { fieldName: boolean } format:
const errors = err.inner.reduce((acc, error) => {
return {
...acc,
[error.path]: true,
}
}, {})
// Update form errors state:
setErrors((prevErrors) =>
update(prevErrors, {
$set: errors,
})
)
})
}
},
[values]
)
// ... rest of the code
Putting the App component together
Now, we can put all these pieces for the logic together, add the render part with <Form />
component, and we are done, almost.
// Import memo, useCallback and useState hooks:
import { memo, useCallback, useState } from 'react'
// Import update method and yup:
import update from 'immutability-helper'
import * as yup from 'yup'
// Import Form component:
import { Form } from './form'
// Create validation schema:
const formSchema = yup.object().shape({
firstName: yup.string().required(),
lastName: yup.string().required(),
email: yup.string().email().required(),
password: yup.string().min(8).required(),
website: yup.string().url().required(),
})
// Create the App component:
export const App = memo(() => {
// Create state for form values:
const [values, setValues] = useState({
firstName: '',
lastName: '',
email: '',
password: '',
website: '',
})
// Create state for form errors:
const [errors, setErrors] = useState({
firstName: false,
lastName: false,
email: false,
password: false,
website: false,
})
// Create handler for input change event:
const onFieldChange = useCallback((fieldName, value) => {
setValues((prevValues) =>
update(prevValues, {
[fieldName]: {
$set: value,
},
})
)
}, [])
// Create handler for form submit event:
const onSubmit = useCallback(
async (event) => {
// Prevent form from submitting:
event.preventDefault()
// Check the schema if form is valid:
const isFormValid = await formSchema.isValid(values, {
abortEarly: false, // Prevent aborting validation after first error
})
if (isFormValid) {
// If form is valid, continue submission.
console.log('Form is legit')
} else {
// If form is not valid, check which fields are incorrect:
formSchema.validate(values, { abortEarly: false }).catch((err) => {
// Collect all errors in { fieldName: boolean } format:
const errors = err.inner.reduce((acc, error) => {
return {
...acc,
[error.path]: true,
}
}, {})
// Update form errors state:
setErrors((prevErrors) =>
update(prevErrors, {
$set: errors,
})
)
})
}
},
[values]
)
// Render the form:
return (
<Form
values={values}
errors={errors}
onFieldChange={onFieldChange}
onSubmit={onSubmit}
/>
)
})
The main component (index)
There is one last thing to do. We need to create the component that will render everything we've built so far. This main component will import the <App />
component and render it in a rootElement
, which will be div in the main HTML file. Now we are done.
// Import React StrictMode and ReactDOM:
import { StrictMode } from 'react'
import ReactDOM from 'react-dom'
// Import App component:
import { App } from './app'
// Import any CSS styles:
import './styles.css'
// Render the React app in the DOM:
const rootElement = document.getElementById('root')
ReactDOM.render(
<StrictMode>
<App />
</StrictMode>,
rootElement
)
Conclusion: How to create custom form validation in React with Yup
Creating custom form validation doesn't have to be hard. Nor does it require using frameworks and all-in-one solutions. There are also smaller libraries, such as Yup, that will help you create custom validation for your forms easily and quickly. I hope that this tutorial helped you understand how to do this.
Top comments (1)
Is it not better to drop fields altogether and to let AI server side deduct what the user wants? The above is the reason why I could not go into London City with my car and wheelchair-bound son, as the form didn't accept my zipcode.