Sooner or later you will need to deal with forms in React! Let's check how to deal with things like form data-validations! 👀
In this tutorial, we will cover how to do work with forms and do data-validation in React. Using a library called react-hook-form.
If you are not sure how to use forms with React, check out these 2 basic React forms patterns.
react-hook-form is a library to make dealing with data in forms easy. Data validation is especially easy when using react-hook-form.
Overview:
Let's get into it right away! 💪
Let's start with how the basic validation may look like. 👇
import React from 'react';
import useForm from 'react-hook-form';
const LoginFormWithValidation = () => {
const {
handleSubmit, // Submit handler wrapper
register, // Register form fields
errors // Errors object including error messages
} = useForm();
const onSubmit = values => {
console.log(values); // email & password input's values in an object.
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="email">Email:</label>
<input
name="email"
type="email"
aria-describedby="emailError"
ref={register({
required: 'Email Message Required Message', // Error message when field is left empty.
pattern: { // Validation pattern
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
message: 'invalid email address' // Error message when validation fails.
}
})}
/>
{errors.email ? (
//
<span id="emailError">{errors.email.message}</span>
) : (
''
)}
<label htmlFor="password">Password:</label>
<input
name="password"
type="password"
ref={register({
validate: value => value !== 'test123' || 'Too common password, you can do better!' // Validation error message
})}
aria-describedby="passwordError"
/>
{errors.password ? (
<span id="passwordError">{errors.password.message}</span>
) : (
''
)}
<input type="submit" value="Submit" />
</form>
);
};
export default LoginFormWithValidation;
At this point, the main focus is the validation. For now, we will be using regular HTML inputs here. 😇
Let see next how we can share the state & validation rules across nested form elements.
react-hook-form takes advantage of React Context. It allows you to provide a form context using FormContext. And read the context using a hook called useFormContext.
Since we are talking about the Context API! Check out how to do state management in React using React hooks and the Context API. 🤓
import React from 'react';
import useForm, { FormContext } from 'react-hook-form';
import EmailInput from './EmailInput';
const LoginFormWithNestedInput = () => {
const methods = useForm();
const onSubmit = values => {
console.log(values);
};
return (
// Initialise context with all form hook methods
<FormContext {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
{/* EmailInput is a nested input field. */}
<EmailInput name="email"></EmailInput>
<label htmlFor="password">Password:</label>
<input
name="password"
type="password"
ref={methods.register({
validate: value => value !== 'test123' || 'You can do better'
})}
aria-describedby="passwordError"
/>
{methods.errors.password ? (
<span id="passwordError">{methods.errors.password.message}</span>
) : (
''
)}
<input type="submit" value="Submit" />
</form>
</FormContext>
);
};
export default LoginFormWithNestedInput;
So far so good, what about the EmailInput component you wonder? 🧐
Here is how it looks like...
import React, { Fragment } from 'react';
import { useFormContext } from 'react-hook-form';
const EmailInput = props => {
const { register, errors } = useFormContext(); // Regular form methods such as register is available from form context.
return (
<Fragment>
<label htmlFor={props.name}>Email:</label>
<input
name={props.name}
type="email"
aria-describedby={`${props.name}-emailError`}
ref={register({
required: 'Required',
pattern: {
// Allows only nordschool.com emails
value: /^[A-Z0-9._%+-]+@nordschool.com/i,
message: 'Invalid email address - Only Nordschool domain is allowed'
}
})}
/>
{errors[props.name] ? (
<span id={`${props.name}-emailError`}>
{errors[props.name].message}
</span>
) : (
''
)}
</Fragment>
);
};
export default EmailInput;
A common use-case for form validations is to use validation schemas.
react-hook-form allows declaring validation schemas. Using another validation library called yup we can define validation rules.
Yup is a JS object schema validator and object parser. The API is similar to Joi but smaller and more performant which makes it a good-fit for client-apps.
Using this setup, here is how a simple sign-up form may look like 🙌....
import React from 'react';
import useForm, { FormContext } from 'react-hook-form';
import EmailInput from './EmailInput';
import AddressInputs, { AddressSchema } from './AddressInputs';
import { string as yupstring, object as yupobject } from 'yup';
const SignupFormSchema = yupobject().shape({
email: yupstring()
.required('Email is unfortunately required')
.email('Please add a real email'),
name: yupstring().required('Name is important, what should we call you?'),
...AddressSchema // Custom schema imported from address inputs.
});
const SignupForm = () => {
const methods = useForm({ validationSchema: SignupFormSchema });
const onSubmit = values => {
console.log(values);
};
return (
<FormContext {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<label htmlFor="name"> Name:</label>
<input
name="name"
type="text"
aria-describedby="nameError"
ref={methods.register()}
/>
{methods.errors.name ? (
<span id="nameError">{methods.errors.name.message}</span>
) : (
''
)}
<EmailInput name="email"></EmailInput>
<AddressInputs name="email"></AddressInputs>
<input type="submit" value="Search" />
</form>
</FormContext>
);
};
export default SignupForm;
And the AddressInputs with their custom schema...
import React, { Fragment } from 'react';
import { useFormContext } from 'react-hook-form';
import { string as yupstring } from 'yup';
export const AddressSchema = {
streetAddress: yupstring().required('Street address is required!'),
postalCode: yupstring()
.length(4)
.required('required!'),
city: yupstring().required('City is required!')
};
const AddressInputs = props => {
const { register, errors } = useFormContext();
return (
<Fragment>
<label htmlFor="stressAddress">Street Address:</label>
<input
name="streetAddress"
type="text"
aria-describedby="streetAddressError"
ref={register()}
/>
{errors.streetAddress ? (
<span id="streetAddressError">{errors.streetAddress.message}</span>
) : (
''
)}
<label htmlFor="postalCode">Postal Code:</label>
<input
name="postalCode"
type="text"
aria-describedby="postalCodeError"
ref={register()}
/>
{errors.postalCode ? (
<span id="postalCodeError">{errors.postalCode.message}</span>
) : (
''
)}
<label htmlFor="city">City:</label>
<input
name="city"
type="text"
aria-describedby="cityError"
ref={register()}
/>
{errors.city ? <span id="cityError">{errors.city.message}</span> : ''}
</Fragment>
);
};
export default AddressInputs;
If you noticed we are repeating that input field pattern all over the place! Let's encapsulate the input field elements in an own component. 😎
import React, { Fragment } from 'react';
const InputField = props => {
return (
<Fragment>
<label htmlFor={props.name}>{props.label}</label>
<input
name={props.name}
type={props.type || 'text'}
aria-describedby={`${props.name}Error`}
ref={props.registerFn}
/>
{props.error ? (
<span id={`${props.name}Error`}>{props.error.message}</span>
) : (
''
)}
</Fragment>
);
};
export default InputField;
Now our AddressInputs could be refactored to look more like this...
import React, { Fragment } from 'react';
import { useFormContext } from 'react-hook-form';
import InputField from './InputField';
import { string as yupstring } from 'yup';
export const AddressSchema = {
streetAddress: yupstring().required('Street address is required!'),
postalCode: yupstring()
.length(4)
.required('required!'),
city: yupstring().required('City is required!')
};
const AddressInputs = props => {
const { register, errors } = useFormContext();
return (
<Fragment>
<InputField
label="Street Address:"
name="stressAddress"
error={errors.streetAddress}
registerFn={register()}
></InputField>
<InputField
label="Postal Code:"
name="postalCode"
error={errors.postalCode}
registerFn={register()}
></InputField>
<InputField
label="City:"
name="stressAddress"
error={errors.city}
registerFn={register()}
></InputField>
</Fragment>
);
};
export default AddressInputs;
Much nicer! 👌
Ok so far so good, now you know enough to get you pretty far!
What about using a component library?
Let have a quick peek on how we can use react-hook-form and material-ui together.
import React from 'react';
import useForm from 'react-hook-form';
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
import { string as yupstring, object as yupobject } from 'yup';
const ContactFormSchema = yupobject().shape({
email: yupstring()
.required('Email is required')
.email('Please enter a valid email'),
message: yupstring().required('Please tell us how we can help you'),
name: yupstring().required('Name is important, what should we call?')
});
const ContactForm = () => {
const { register, errors, handleSubmit } = useForm({
validationSchema: ContactFormSchema
});
const onSubmit = values => console.log(values);
return (
<form autoComplete="off" onSubmit={handleSubmit(onSubmit)} noValidate>
<TextField
id="name"
label="Name"
name="name"
inputRef={register}
placeholder="Joe"
margin="normal"
variant="outlined"
error={errors.name ? true : false}
helperText={errors.name ? errors.name.message : ''}
/>
<TextField
id="email"
label="Email"
name="email"
inputRef={register}
placeholder="example@nordschool.com"
margin="normal"
variant="outlined"
error={errors.email ? true : false}
helperText={errors.email ? errors.email.message : ''}
/>
<TextField
required
id="message"
multiline
rows="4"
name="message"
inputRef={register}
label="How can we help you today?"
placeholder="Some pizza please!"
margin="normal"
variant="outlined"
error={errors.message ? true : false}
helperText={errors.message ? errors.message.message : ''}
/>
<Button variant="contained" type="submit">
Submit
</Button>
</form>
);
};
export default ContactForm;
That is it, now your basic form-validation training is complete! 🎖️
Enjoyed the article? Share the summary thread on twitter.