Originally posted on Nordschool.
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:
- With Basic validation
- Validation For Nested Fields
- Validation Schemas with Yup
- Custom Input Field
- Validation with Material-UI
Let's get into it right away! πͺ
With Basic validation
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.
Validation For Nested Fields
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.
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. π
Custom Input Field
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.
Validation with Material-UI
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! ποΈ
Support
Enjoyed the article? Share the summary thread on twitter.
Nordschool@nordschoolLet's check how to do work with forms and do data-validation in React. Using @HookForm. π
- With Basic validation
- Validation For Nested Fields
- Validation Schemas with Yup
- Validation with @MaterialUI
#NordschoolTutorial
THREADβ¦ π13:04 PM - 07 Nov 2019
Better Code Monday Newsletter
You might also like my newsletter. The idea is to share 3 web dev tips every Monday.
My goal is to improve my writing skills and share knowledge as much as possible. So far, few hundreds of developers have subscribed and seem to like it.
To get a feeling of what kind of stuff I share, Check out the previous newsletter issues and subscribe.
Top comments (1)
Thank you for sharing this, the useFormContext it's very very handy!