React hooks forms and data schema validation
...a quick walkthrough on form common validations scenarios using yup
Hi, this is a quick overview of how to implement form validations, is not a secret, this is a common task, mostly any developer someday faces it. There are many validations vendors outside: Redux Forms, Formik, React Final Forms and React Hook Form .
We will talk about React Hook Form , I picked this because what I've seen, is the one that looks more natural for the React hooks ecosystem, source code is written in Typescript as well โ๏ธ.
Another cool advantage is that you don't need special form components or marry a specific library or library to wire your validations, it is so minimalistic that only requires ref
to the DOM input element or if the UI library of your preference exposes the ref
, that's the way to go.
We will work together with Yup, which is an object schema validation, I have used before in a couple of projects, there is huge documentation, and will let you achieve even complex edge cases you can think of, like validations based on other fields, an array with compound object validations, and more.
Requirements
- A bit familiar with React and Typescript
So what will cover in this blog post?
- Setting up React Hook Form
- Basics with Simple login form validation
- Validate referencing other fields
- Controlled and reusable fields
- Async validations
- Internalization of error messages
- Conclusions
Setting up React Hook Form
We'll use CRA with Typescript https://create-react-app.dev/docs/adding-typescript/
, because is so easy to start coding, but I will keep track of the samples here so you can check the finished code codesandbox.io/s/react-and-form-validations..
Given this, we need to install react-hook-form
, because is the main dependency but as I mentioned before, we are going to use object schema validation, that's why we also need yup
github.com/jquense/yup and react hook form resolvers @hookform/resolvers
Basics with Simple Login Form Validation
As simple as it is a login form validation, we can make our code quite robust mixing types and yup schemas.
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
type LoginAttributes = {
username: string;
password: string;
};
const schema: yup.SchemaOf<LoginAttributes> = yup.object().shape({
username: yup.string().email().required(),
password: yup.string().required()
});
export const Login = () => {
const { register, handleSubmit, errors } = useForm({
resolver: yupResolver(schema)
});
const onSubmit = (data: LoginAttributes) => alert(JSON.stringify(data));
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="text" name="username" ref={register} />
<p>{errors.username?.message}</p>
<input type="password" name="password" ref={register} />
<p>{errors.password?.message}</p>
<input type="submit" />
</form>
);
};
On this sample, we have LoginAttributes
that way we map the yup schema to a regular type using yup.SchemaOf
but also we can use it to type the onSubmit
parameter, easy... . On the native onSubmit
is called the handleSubmit
provided by useForm
.
To register each input to the validation we use React ref
, if you are playing with React Material or ChakraUI, you can check its documentation on how to access the ref of the input DOM elements.
The error messages live on the errors
map, it contains errors for all the fields accessible by the key we defined.
By default React Hook Form shows up validation on submit, if we want to change this behavior on useForm
configuration there is the mode
attribute, like to onTouched
so when field is touched and blurred it shows the error message.
const { register, handleSubmit, errors } = useForm({
mode: "onTouched",
resolver: yupResolver(schema)
});
Reference other fields
One common case is the user registration with password confirmation, Yup provides a simple way to access other fields:
type RegistrationAttributes = {
username: string;
password: string;
passwordConfirmation: string;
};
const schema: yup.SchemaOf<RegistrationAttributes> = yup.object().shape({
username: yup.string().email().required(),
password: yup.string().required(),
passwordConfirmation: yup
.string()
.oneOf([yup.ref("password"), null, ""], "Passwords must match")
.required()
});
The oneOf
validation method restricts input value to be one of the values in the list only, but you can see also yup.ref("password")
, so we are telling that passwordConfirmation
has to be the value of password
field. However we need to add null
and ""
to make the required()
validation also work. Finally this validation only passes if input value is exactly as password
.
Note: This is also possible with .test( ... ) method, but this approach is more verbose and maybe useful in other more complex scenarios.
Async validation
Sometimes we have to validate against a service, for example, a credit card or bank number, this type of actions are not instant so it has to wait sometime in order to receive a response from the service, here is where async validations come to rescue and Yup supports really well:
Account attributes
type BankAccountAttributes = {
accountNumber: string;
};
Yup provides a .test(...) method to let us validate asynchronously the field, it receives parameters: validation name, message, and async callback, we are returning a Promise here because validateBankFunction is debounced :
const schema: yup.SchemaOf<BankAccountAttributes> = yup.object().shape({
accountNumber: yup
.string()
.required("Your bank account is required")
.test(
"valid-bank-account",
"Your bank account is not active",
async (value, testContext) =>
// debounced function does not return Promise, can't use async/await
new Promise((resolve) =>
validateBankAccount(value, testContext, resolve)
)
)
});
As you can see we are using lodash to debounce the function
const validateBankAccount = debounce(
(
value: string,
testContext: yup.TestContext,
resolve: (val: boolean) => void
) => {
// simulate HTTP request
setTimeout(() => {
if (value === "000000") {
return resolve(false);
}
return resolve(true);
}, 600);
},
800
);
From useForm
also is possible to use the form state isValidating
to disable submit button if you need to:
const {
register,
handleSubmit,
errors,
formState: { isValidating, isValid }
} = useForm({
mode: "all",
resolver: yupResolver(schema)
});
Sample: Bank number validation
Controlled and reusable fields
Often we faced scenarios reusing same fields or group of fields, one use case, the address, it does not matter it is your home address or billing address, even though those 2 addresses are different they have the same inputs, let's see how to achieve this, first would be good to install error component, helps a lot on these nested scenarios, npm install @hookform/error-message
,
we still need a base type and the form validations:
type AddressAttributes = {
country: string;
billingAddress: {
zipCode: string;
street: string;
};
};
const schema: yup.SchemaOf<AddressAttributes> = yup.object().shape({
country: yup.string().required(),
billingAddress: yup.object().shape({
street: yup.string().required(),
zipCode: yup
.string()
.matches(/^[0-9]+$/, "Invalid zip code")
.required()
})
});
but we will use the controller capabilities that lets you isolate the field, we need the control and name of this field as props passed from the parent component and initialize the useController
hook for each of the fields belonging to address:
const AddressControl = ({ control, name }: UseControllerOptions) => {
const zipCode = useController({
name: `${name}.zipCode`,
control,
defaultValue: ""
});
const street = useController({
name: `${name}.street`,
control,
defaultValue: ""
});
return (
<>
<div>
<input type="text" {...zipCode.field} />
<ErrorMessage
name={`${name}.zipCode`}
render={({ message }) => <p>{message}</p>}
/>
</div>
<div>
<input type="text" {...street.field} />
<ErrorMessage
name={`${name}.street`}
render={({ message }) => <p>{message}</p>}
/>
</div>
</>
);
};
finally we are able to have 2 times the same component from the parent but using different names:
return (
<FormProvider {...formMethods}>
<form onSubmit={handleSubmit(onSubmit)}>
<select name="country" ref={register}>
<option value="">Select a country</option>
<option value="CR">CR</option>
<option value="US">US</option>
</select>
<p>{errors.country?.message}</p>
<h3>home address</h3>
<AddressControl control={control} name="homeAddress" />
<h3>billing address</h3>
<AddressControl control={control} name="billingAddress" />
<input type="submit" />
</form>
</FormProvider>
if you noticed, also we used FormProvider
initialized with formsMethods
which is the object returned by the useForm
, ok the FormProvider
makes it possible to use the ErrorMessage
and obtain the errors triggered from the field component and also if you need to use the useFormContext
(no in this case), code source here.
Internalization of error messages
Yup comes with bundled default messages for its validation functions, however, we can pass a custom message through the function or even use its customization strategy for localization rewriting the locale dictionary. Personally I prefer the former approach, passing a custom key into the validation functions, for example if we have this:
const schema: yup.SchemaOf<LoginAttributes> = yup.object().shape({
username: yup.string().email().required(),
password: yup.string().required()
});
We can pass the translation keys you set in your app localization mechanism:
const schema: yup.SchemaOf<LoginAttributes> = yup.object().shape({
username: yup.string().email('form.errors.email').required('form.errors.required'),
password: yup.string().required('form.errors.required')
});
Then if you are using a library like react 18next, can call the translation function from the component:
<input type="text" name="username" ref={register} />
<p>{t(errors.username?.message)}</p>
Additionally ${path}
can be used in the error message to get the field name , can be used like 'form.errors.${path}'
Conclusions
This is just a summary of common cases, but react hook form and yup goes beyond, with a lot of more validation mechanisms.
Other samples in my sandbox
useFormContext
helps to build deeply nested forms into separate components.- Working with fields lists
For more examples, I created for this post visit this code sandbox . If you enjoyed this post please like ๐ and share ๐ !!