How to Build a Custom Hook for Form Validation in React

DevOps Engineer
July 17, 2024
0 MIN READ
#react-native#cicd#mobile-dev#backend#build

Here's your blog post on building a custom hook for form validation in React:


Form validation is a critical aspect of modern web applications, ensuring data integrity and improving user experience. While there are excellent libraries like Formik and React Hook Form available, sometimes you need a lightweight, custom solution tailored to your specific requirements. In this post, we'll walk through building a custom React hook for form validation that you can reuse across your application.

Why Build a Custom Form Validation Hook?

Before diving into implementation, let's consider why you might want to create your own validation hook:

  1. Lightweight solution: Avoid adding another dependency to your project
  2. Customizable validation rules: Tailor validation to your exact needs
  3. Better understanding: Deepen your knowledge of React hooks and form handling
  4. Reusability: Create a solution you can use across multiple components
  5. Performance: Optimize for your specific use cases

Our custom hook will handle form state management, validation, and error messages while providing a clean API for your components to consume.

Designing the useFormValidation Hook

Let's start by designing our hook's API. We want it to:

  • Manage form values
  • Validate fields based on rules
  • Track touched/dirty state
  • Provide error messages
  • Handle form submission

Here's the basic structure of our useFormValidation hook:

import { useState, useEffect } from 'react'; const useFormValidation = (initialState, validate, onSubmit) => { const [values, setValues] = useState(initialState); const [errors, setErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); const [touched, setTouched] = useState({}); // Validation effect useEffect(() => { if (isSubmitting) { const noErrors = Object.keys(errors).length === 0; if (noErrors) { onSubmit(values); } setIsSubmitting(false); } }, [errors, isSubmitting, onSubmit, values]); const handleChange = (e) => { const { name, value } = e.target; setValues({ ...values, [name]: value, }); // Mark field as touched setTouched({ ...touched, [name]: true, }); }; const handleBlur = (e) => { const { name } = e.target; setTouched({ ...touched, [name]: true, }); }; const handleSubmit = (e) => { e.preventDefault(); const validationErrors = validate(values); setErrors(validationErrors); setIsSubmitting(true); }; return { values, errors, touched, isSubmitting, handleChange, handleBlur, handleSubmit, }; }; export default useFormValidation;

Implementing Validation Rules

The real power of our hook comes from the validation rules we define. Let's create a separate validation function that we'll pass to our hook.

Here's an example validation function for a login form:

const validateLogin = (values) => { let errors = {}; // Email validation if (!values.email) { errors.email = 'Email is required'; } else if (!/\S+@\S+\.\S+/.test(values.email)) { errors.email = 'Email address is invalid'; } // Password validation if (!values.password) { errors.password = 'Password is required'; } else if (values.password.length < 6) { errors.password = 'Password must be at least 6 characters'; } return errors; };

This validation function checks for:

  1. Required fields
  2. Valid email format
  3. Minimum password length

You can extend this with more complex rules like password strength, matching fields, or async validation.

Using the Hook in a Component

Now let's see how to use our custom hook in a login form component:

import React from 'react'; import useFormValidation from './useFormValidation'; import validateLogin from './validateLogin'; const LoginForm = () => { const initialState = { email: '', password: '', }; const { values, errors, touched, isSubmitting, handleChange, handleBlur, handleSubmit } = useFormValidation(initialState, validateLogin, loginUser); async function loginUser() { // Handle actual login logic here console.log('Logging in with:', values); } return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="email">Email</label> <input type="email" name="email" id="email" value={values.email} onChange={handleChange} onBlur={handleBlur} className={touched.email && errors.email ? 'error' : ''} /> {touched.email && errors.email && ( <span className="error-message">{errors.email}</span> )} </div> <div> <label htmlFor="password">Password</label> <input type="password" name="password" id="password" value={values.password} onChange={handleChange} onBlur={handleBlur} className={touched.password && errors.password ? 'error' : ''} /> {touched.password && errors.password && ( <span className="error-message">{errors.password}</span> )} </div> <button type="submit" disabled={isSubmitting}> {isSubmitting ? 'Logging in...' : 'Login'} </button> </form> ); }; export default LoginForm;

Enhancing the Hook with Advanced Features

Our basic implementation works well, but we can improve it with some additional features:

  1. Dirty fields tracking: Only show errors after the user has interacted with a field
  2. Async validation: Support for server-side validation
  3. Field-level validation: Validate individual fields on blur
  4. Reset functionality: Clear the form when needed

Here's how we might add these enhancements:

const useFormValidation = (initialState, validate, onSubmit) => { // ...previous state declarations... const [dirty, setDirty] = useState({}); // Add field-level validation const validateField = (name, value) => { const fieldErrors = validate({ [name]: value }); setErrors(prev => ({ ...prev, [name]: fieldErrors[name] })); }; const handleChange = (e) => { const { name, value } = e.target; setValues(prev => ({ ...prev, [name]: value, })); setDirty(prev => ({ ...prev, [name]: true, })); // Optional: validate on change if (touched[name]) { validateField(name, value); } }; const handleBlur = (e) => { const { name, value } = e.target; setTouched(prev => ({ ...prev, [name]: true, })); validateField(name, value); }; const resetForm = () => { setValues(initialState); setErrors({}); setTouched({}); setDirty({}); setIsSubmitting(false); }; return { values, errors, touched, dirty, isSubmitting, handleChange, handleBlur, handleSubmit, resetForm, }; };

Conclusion

Building a custom form validation hook in React gives you fine-grained control over your form's behavior while maintaining a clean, reusable implementation. Our useFormValidation hook handles the core requirements of form management:

  • State management for form values
  • Validation logic execution
  • Error state tracking
  • Submission handling
  • User interaction tracking (touched/dirty states)

The beauty of this approach is its flexibility. You can:

  • Swap validation rules per form
  • Extend with additional features as needed
  • Maintain consistency across your application
  • Avoid external dependencies

Remember that while custom solutions are great for learning and specific use cases, established libraries like React Hook Form or Formik might be better choices for complex forms in production applications. However, understanding how to build your own solution makes you a better React developer and helps you appreciate what these libraries do under the hood.

Try implementing this hook in your next project and see how it compares to other solutions you've used!

Share this article