Build a form validation engine using custom React Hooks, from scratch, without having to learn a single form library. Read on to learn how!

In part one, Simplify Forms using Custom React Hooks, we abstracted away all of the form event handler logic into a custom React Hook. As a result, the code in our form components was reduced by a significant amount.

After publishing last week’s tutorial, I had a number of readers ask how I’d use React Hooks to solve two common problems related to forms:

  • Initializing the form values
  • Validation

Therefore, I’ll be answering these questions in this tutorial. So, let’s begin learning how to initialize form values and handle form validation using React Hooks!

What We’re Building

A login form with validation using a custom react hook.

We’ll be using the same project from part one. If you haven’t yet gone through the first tutorial on how to Simplify Forms with Custom React Hooks. Or, you can grab the full code and continue with this tutorial.

There’s plenty of form libraries available for React. They do a great job of simplifying your code. However, by using a library, you’re adding to the (already long) list of dependencies your project relies on.

Libraries are also opinionated. You have to learn how that library works, as well as its limitations.

The goal of this tutorial is to walk you through writing your own custom React Hook that handles form validation for you. We’re going to start with initialization.

Initializing the Form Values

Actually, handling form initialization doesn’t require our custom React Hook, useForm, but it’s still an important part of the validation process.

Start by opening up the original project in your text editor, open Form.js, and take a look at the HTML that’s being returned towards the bottom of the component, specifically the email input field:

<input className="input" type="email" name="email" onChange={handleChange} value={} required />

Let’s take a closer look at the value attribute. We pass in the email key returned from the values object that’s stored in the useForm custom Hook.

In the React world, because we’re handling the input’s value ourselves, this means our email input field is a controlled input.

Well, not exactly.

The email input does become a controlled input, eventually, when we pass a real value to it.

When the Form component first renders, it initializes the useForm custom React Hook. Go ahead and open up useForm.js and look at the initial state of values inside of that custom Hook:

const [values, setValues] = useState({});

We’re initializing the values state to an empty object. As a result, when our Form component gets, it doesn’t find it inside values and therefore is undefined.

So, our email input field starts off with a value of undefined, but when we type a value inside of the input, useForm finally sets the value of email inside of its state to be a non-undefined value.

That’s not great. What we’re doing is switching from an uncontrolled input to a controlled input.

We get a big error message from React for doing that. Bad developer, bad!

|| to the Rescue

We’ve all seen, and perhaps even used the operator above, ‘||‘, inside of a conditional statement. That’s right, it’s OR.

Therefore, we’re going to use the OR operator to set the default value of the email input, like so:

<input ... value={ || ''} ... />

I sometimes think it’s helpful to explain code in plain English. So, what we’re saying above is: set the value of this input to be OR if doesn’t exist, set it to be an empty string.

In other words, we initialize the default value of the input to an empty string. I want to add that we’re not limited to using an empty string. If it’s a number input, we’d use 0, for example.

Setting Up Form Validation Using React Hooks

Now that we’ve tackled initializing the form values, let’s move on to extending our custom React Hook to handle form validation.

We need to do several things in order to validate a form:

  • Define validation rules for the form
  • Store any errors in a state variable
  • Prevent the form from submitting if any errors exist

Defining the Validation Rules

Start by creating a new file for us to define rules for our email and password fields.

Each form will have a list of rules that are specific to its input fields, so name the new file something specific, like LoginFormValidationRules.js.

Add a single function called validate which takes one parameter, values, export it as the default value, and initialize a new object inside of the validate function called errors.

we’ll return the error object at the end of the function so we can enumerate over the errors inside of the useForm custom Hook.

export default function validate(values) { let errors = {}; return errors; };

Let’s add a validation rule for the email input field. The first rule, that’s likely going to apply to every required field in your form, will be to check that the value actually exists.

export default function validate(values) { let errors = {}; if (! { = 'Email address is required'; } return errors; };

Because we’re building an object of errors, we actually check if the email value does not exist, and if so, then we add a new key to the error object called email. Finally we set its value to be ‘Email address is required’.

For an email to be correct however, it has to be written in a specific way, Usually

How can we easily check that the email address is typed in the correct format?

I’m going to say a phrase that makes even the most hardened developer shudder with dread, but please, hear me out.


If you’re like me, you won’t ever learn how to write a regular expression, and instead search for one online like a normal developer. A great site is, which has thousands of useful examples.

After the end of the first if clause, add an else if clause that tests the value of email against a regular expression.

export default function validate(values) { let errors = {}; if (! { = 'Email address is required'; } else if (!/\S+@\S+\.\S+/.test( { = 'Email address is invalid'; } return errors; };

Great! We’ve now defined a list of form validation rules that can be plugged into any number of React Hooks, so let’s test them out. If you want to learn more about hooks in React check out this guide.

Using Form Validation Rules inside of React Hooks

Jump over to the Form component, inside Form.js. We initialize the useForm custom React Hook at the top of the component body. Let’s pass our validate function to the useForm Hook as the second parameter:

... import validate from './LoginFormValidationRules'; const Form = () => { const { values, handleChange, handleSubmit, } = useForm(login, validate); ...

Next, head over to our custom React Hook, at useForm.js.

Add the new validate parameter inside of the useForm function’s parentheses:

... const useForm = (callback, validate) => { ...

Remember, validate takes an object, values, and returns another object, errors.

For the Form component to display a list of errors, our useForm custom React Hook needs to store them in its state. Therefore, let’s declare a new useState Hook under values, called errors:

const [errors, setErrors] = useState({});

Finally, when a user submits the form, we first want to check that there are no issues with any of their data before submitting it.

Change the handleSubmit function to call validate instead of callback, passing in the values stored in the Hook’s state.

const handleSubmit = (event) => { if (event) event.preventDefault(); setErrors(validate(values)); };

Detecting Change in Errors State

We’re setting the errors state to the result of validate, but nowhere are we actually submitting the form. We need to add back the call to the callback function to our useForm Hook. But where?

Enter the useEffect Hook.

useEffect replaces the componentDidMount and componentDidUpdate lifecycle methods in React Class components.

Furthermore, by passing an array with a value inside as the second parameter to useEffect, we can tell that specific useEffect declaration to run whenever that value changes.

Let’s add a useEffect Hook that listens to any changes to errors, checks the length of the object, and calls the callback function if the errors object is empty:

useEffect(() => { if (Object.keys(errors).length === 0) { callback(); } }, [errors]);

The useEffect above is essentially saying, as a side effect of the value of errors changing, check if the errors object contains any keys (if it’s empty) and if so, call the callback function.

It took me a while to wrap my head around the naming of the useEffect Hook, but if you think about it like: “as a result (side effect) of [value] changing, do this”, it makes much more sense.

Preventing the Form from Submitting on Render

Before we move on to the final section, hooking up the form HTML to the errors, there’s a problem with the login function inside our Form component. It’s being called when the page loads.

This is because our useEffect Hook above is actually being run once when the component renders because the value of errors is initialized to an empty object.

Let’s fix this by adding one more state variable inside of our custom React Hook, called isSubmitting. Set the initial state to false.

... const [values, setValues] = useState({}); const [errors, setErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); ...

Then setIsSubmitting to true inside handleSubmit.

const handleSubmit = (event) => { if (event) event.preventDefault(); setIsSubmitting(true); setErrors(validate(values)); };

Check that isSubmitting is true inside of the useEffect Hook:

useEffect(() => { if (Object.keys(errors).length === 0 && isSubmitting) { callback(); } }, [errors]);

Finally, return the errors object at the bottom of the Hook:

return { handleChange, handleSubmit, values, errors, }

Your finished useForm Hook should now look like this:

import { useState, useEffect } from 'react'; const useForm = (callback, validate) => { const [values, setValues] = useState({}); const [errors, setErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { if (Object.keys(errors).length === 0 && isSubmitting) { callback(); } }, [errors]); const handleSubmit = (event) => { if (event) event.preventDefault(); setErrors(validate(values)); setIsSubmitting(true); }; const handleChange = (event) => { event.persist(); setValues(values => ({ ...values, []: })); }; return { handleChange, handleSubmit, values, errors, } }; export default useForm;

Displaying Errors in the Form Component

Now our custom React Hook is saving a list of errors, let’s display them for our users to see. This is the final step to adding some proper form validation inside of any custom React Hooks.

First, make sure to add errors to the list of variables and functions we’re getting from useForm:

const { ... errors, ... } = useForm(login, validate);

Bulma (the CSS framework we’re using) has some excellent form input classes that highlight inputs red.

Let’s make use of that class by checking if the errors object has a key that matches the input name, and if so, adds the is-danger class to the input’s className:

<div className="control"> <input className={`input ${ && 'is-danger'}`} type="email" name="email" onChange={handleChange} value={ || ''} required /> </div>

Finally, display the actual error message by adding an inline conditional below the input element to check again if the errors object has a key matching this input, and if so, displays the error message in red:

<div className="control"> <input className={`input ${ && 'is-danger'}`} type="email" name="email" onChange={handleChange} value={ || ''} required /> { && ( <p className="help is-danger">{}</p> )} </div>

Save everything, jump on over to your app running in your browser (npm start in the project if you didn’t do so already) and take your new form for a test run!

A login form validation powered by a custom react hook

Adding Additional Validation Rules

“Where’s the password validation?”, you might be thinking. 

I’m going to leave that part for you to add. Homework as it were. If you want the solution, you can check out the entire code base for this tutorial.

Wrapping Up

So there you have it, form validation and initialization using custom React Hooks. As always, if you enjoyed the tutorial, please leave a message in the discussion below.

If you have any issues or questions, leave a comment below or hit me up on Twitter. It’d be my pleasure to help. Peace!

Additional Reading

👋 Hey, I'm James Dietrich
I work full-time at an AI-based startup out of San Francisco, CA. My true passion is to help others. My tutorials help 150,000+ developers learn React and JavaScript every month. Follow on Twitter, or Github.

💬 Leave a comment

Your email address will not be published. Required fields are marked *

We will never share your email with anyone else.


Hey James, thanks for this article!

I’m wondering how you’d tackle this use case – I have a form that I use for both creating and editing. When it first loads it’s in “create mode”, but if I select an item elsewhere on the page, I want to reinitialise the fields with that item’s values (“edit mode”). Assume that the new initial values are being passed in as props to the form component.

How would you go about reinitialising in this way using hooks?

Thank you for this post, I love this approach 😉
But what I don’t like though is initialising the values from the value attribute by using the syntax you suggested: value={ || ”}
Just want to have value={}

So I wanted to have some initialValues there. so I forked it and changed it so I now pass initialValues as the first parameter into useForm. I also made the validate function optional.

See the fork here:

What do you think?

I made useForm initialize using values from a prop. One thing to note is to have a useEffect that updates the state of values each time the prop changes else it uses the old value and introduces bug.
useEffect(() => {

Good example! I have one question. By this structure you are assuming that your form invalid initially, and that’s ok. From other side your are triggering validation on submit only, right? Would it better to use instead or additionally input value change or onBlur event for validation check, so that user could immediately see that he had entered correct value and fixed invalid input issue?

There’s a slight bug within useForm.js – you’re not setting the isFormSubmitting state to false after calling the callback function.
This leads to callback spillover in situations where your callback function is some more advanced logic to be performed, such as a login call to a GraphQL API. Had I not fixed the state setting, my form would keep calling the API with every single change in my inputs(I also use blur events)

I followed a different approach to handle the controlled to uncontrolled issue:

I send an initialdFormfields object to useForm. As far as the form will initialize empty (as in a login) its exactly the same.

But, wether I solve the controlled to uncontrolled problem with my strategy or yours, if you want to initialize the form with previous values depending on a component props, it will fail.

Component consuming useForm:

const ComponentConsumingUseForm = ({ initialFormValuesAsProps }) => {
const initialFormFields = {
lastname: initialFormValuesAsProps.lastname
const { values, errors, handleChange, handleSubmit } = useForm(
() => {

const useForm = (callback, initialFormFields = {}, validate) => {
const [values, setValues] = useState(initialFormFields);

} and values,lastname will be set as undefined, then to its real value, but the UI wont catch up, rendering an empty input.

Is there a solution for that?

Thank you so much in advance.

You can build a form dynamically from JSON two ways: using a library, or writing your own form generator. The decision depends on how comfortable you are with JavaScript and React.

Thanks, Martin! I’ve used the classnames library before and really liked it. It’s also pretty much a must when working with dynamic class names, right? Otherwise you litter the class attribute with false all the time.

Thanks James!
I have extended the hook to support onChange validation. Instead of passing the validate function, I am passing a list of validators in a form of {key: fn}
That way in onChange, we can see if there is a validator for a particular field and run validation then. The onSubmit simply runs all validators.

const useForm = (callback, validators) => {

const handleChange = event => {
const {id, value} =


const validator = validators[id]
if (validator) {
setErrors(errors => ({…errors, [id]: validator(value)}))

setValues(values => ({…values, [id]: value}))

const validate = values => {
let errors = {}
Object.keys(validators).forEach(id => {
const validator = validators[id]
if (validator) {
const error = validator(values[id])
if (error) {
errors[id] = error

return errors


if (isSubmitting && Object.keys(err).length ===0) {
},[err] );

in the above code, callback(); function calling at first time loading, thought isSubmitting is false at first

Thanks, James for the effort you put to bring the awesome content each time ..
Many are remote learners like me …God bless you

Great article, however I have found one issue.

If you use the react-hooks plugin for eslint (recommended by the react team as best practice), you’ll find that your useEffect hook requires the isSubmitting, and callback to be included in your dependency array.

This causes the form to be submitted twice for me (which makes sense, once when isSubmitting is changed to true, and again when setErrors is set coming back as an empty array after validation). Without further changes to your approach, this can only be remedied by disabling eslint for the useEffect, something I don’t want to do.

Any thoughts on how you’d go about fixing this?

Hi Lauchlan,

You need to include a conditional inside of the useEffect Hook to check isSubmitting. The reason for this is because the state variables are initialized when the component mounts, and therefore the useEffect will trigger.

Thank you, sir. I have learned more from your post, but I got this warming: “React Hook useEffect has missing dependencies: ‘callback’ and ‘isSubmitting’. Either include them or remove the dependency array. If ‘callback’ changes too often, find the parent component that defines it and wrap that definition in useCallback”.

I saw your comment about adding a conditional inside of useEffect(), but I still got the same error.

Hi there, many thanks for nice article!

I have added two more lines to your useForm as I wanted to setErrors when the input is changing 😉

import { useState, useEffect } from ‘react’;

export const useForm = (callback, validate) => {

const [values, setValues] = useState({});
const [errors, setErrors] = useState({});
const [isSent, setIsSent] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);

useEffect(() => {
if (Object.keys(errors).length === 0 && isSubmitting && isSent) {
}, [errors]);

const handleSubmit = (event) => {
if (event) event.preventDefault();

const handleChange = (event) => {
event.persist && event.persist();
const newValues = { …values, []: };
isSent && setErrors(validate(newValues));
setValues(values => (newValues));

return {

Hello James, nice tutorial, but i have a little problem. The bulma library is affecting my existing css files on my React hooks project. I’ve tried several ways to separate them.. Is there a way not to let them affect each other? Thank you

In the handleSubmit function after the callback i would reset isSubmitting state to false. But anyway, really great thing!