Form is one of core functionalities that must exist in any application. This functionality allows us interact with user data (input) and process it into a valuable data or resource (output).
Even this functionality is powerful, it has one biggest obstacle. Yes, it's how to validate the user input. And that's the reason why I make this article.
In this article I will share to you how we can handle form validation specifically in React application using a package called react-hook-form
.
Let's start it out!
Requirements
- React application (fresh or existing app)
NOTE:
I will use my previous project calledexample-app
. It has no features. Just a fresh React project installed usingCRA
.
Steps
1. Add react-hook-form
I use Git inside example-app
. So, before adding the package I will create a new branch called feat-signin
then merge that feature into branch main whenever I finish that feature.
# Create and checkout to branch feat-signin
git checkout -b feat-signin
Now, it's time to add the package.
yarn add react-hook-form
2. Create the form
Maybe you have a clue about what form that I want to build. Yes, it's a sign-in form. I will validate the user email and password before they can actually sign-in.
I will not using any styling tools. Just HTML to make it simple and focus :).
First, I wanna add a page called Signin inside my project.
# Here's my current project
.
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── README.md
├── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ ├── pages
│ │ └── Signin.js
│ ├── reportWebVitals.js
│ └── setupTests.js
└── yarn.lock
Create the sign-in form and import it inside App.js
.
// pages/Signin.js
function Signin() {
return (
<div>
<form>
<div>
<label htmlFor="email">Email</label>
<input type="email" id="email" />
</div>
<div>
<label htmlFor="password">Password</label>
<input type="password" id="password" />
</div>
<div>
<button>Signin</button>
</div>
</form>
</div>
);
}
export default Signin;
// App.js
import Signin from './pages/Signin';
function App() {
return <Signin />;
}
export default App;
3. Integrate the form with react-hook-form
Let's integrate the previous form so we can collect all user input inside that form.
// pages/Signin.js
import { useForm } from 'react-hook-form';
function Signin() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const onSubmit = (form) => {
console.log(form);
};
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="email">Email</label>
<input type="email" id="email" {...register('email')} />
</div>
<div>
<label htmlFor="password">Password</label>
<input type="password" id="password" {...register('password')} />
</div>
<div>
<button>Signin</button>
</div>
</form>
</div>
);
}
export default Signin;
Run the application, open the browser console and try to submit the form. You will see something like image below.
4. Add input validation
If I submit the form with an empty password, I will not get any error message that indicate I forgot to input my password which is a bad UX.
To achieve that feature, we need to install two packages. @hookform/resolvers/yup
and yup
.
yarn add @hookform/resolvers yup
NOTE:
yup is one of JavaScript schema Objects. It allows us to define a shape (structure) and validate a JavaScript object. You can also use other schema object like Joi, Zod, etc.
Now, let's add input validation into the form.
// pages/Signin.js
// ...
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
const schema = yup.object().shape({
email: yup.string().email().required(),
password: yup.string().required(),
});
function Signin() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({ resolver: yupResolver(schema) });
// ...
}
// ...
Now, if we submit the form with an empty password, we will not see any message in the browser console because the form is actually throw error messages and not submit the form.
Look, even the cursor automatically focus to the input element that produce the error.
5. Displaying the error messages
Now, the form can validates the input value, but it's not good enough. We need to display what's wrong with the form. So, the user can input the correct value.
// pages/Signin.js
<div>
<label htmlFor="email">Email</label>
<input type="email" id="email" {...register("email")} />
{errors.email?.message && <span>{errors.email.message}</span>}
</div>
<div>
<label htmlFor="password">Password</label>
<input type="password" id="password" {...register("password")} />
{errors.password?.message && <span>{errors.password.message}</span>}
</div>
Submit the form and we can see the error messages.
Hmm, I think the error messages not user friendly enough, right? So, let's improve it.
6. Customize the error messages
This is the reason why I choose Yup for the schema validation. We can easily customize the error messages like this.
// pages/Signin.js
// ...
const schema = yup.object().shape({
email: yup
.string()
.email('Please provide a valid email address')
.required('Please provide your email address'),
password: yup.string().required('Please provide your password'),
});
// ...
Submit the form again and you will see the error messages has changed.
Bonus
Validating the form on the client side is not enough. We also need to validate the form on the server side because attacker can bypass our validation on the client side.
The problem comes when we want to display the error messages from the server into the form. Fortunately, we can easily done this feature using react-hook-form.
We just need to use setError
API to display the error messages that comes from the server into the form.
// pages/Signin.js
// Response from the server
// {
// "message": "...",
// "errors": {
// email: ["The email must be a valid email address."]
// }
// }
// ...
const {
register,
handleSubmit,
formState: { errors },
setError,
} = useForm({ resolver: yupResolver(schema) });
const onSubmit = async (form) => {
// Do sign-in process. Just example :)
await Http.post(/** ... */);
if ((await Http.status()) === 422) {
let res = await Http.response();
for (let [field, messages] of Object.entries(res.errors)) {
setError(field, { type: 'manual', message: message.join(' ') });
}
}
// ...
};
// ...
Top comments (2)
Thank you for the awesome tutorial.
You're welcome, Bill!