Form validation using custom hooks in react/typescript

·

4 min read

Form validation is a chore that every react developer has to contend with in their job, this is why there are many libraries that hide the pain of validating inputs behind beautiful abstractions. The form validation libraries offer plenty of functionality and should be the go to option for react devs who need to ship stuff fast, however, it does not hurt to learn how to create form validation from scratch. In this article I demonstrate how to add form validation using a custom hook.

The form is set up such that when the user clicks submit, they are presented with all the errors, and the form cannot be submitted until the errors are fixed.

The core part of the validation is this function

export interface Ivalues{
    username: string, 
    email: string, 
    password: string,
    password2: string
}

export default function validateInfo(values:Ivalues){
    const errors: Partial<Ivalues> = {}

    if(!values.username.trim()){
        errors.username = "Username Required"
    }


    if(!values.email){
        errors.email = "Email Required"
    }else if(!/\S+@\S+\.\S+/.test(values.email)){
        errors.email = "Email address provided is invalid"
    }


    if(!values.password){
        errors.password = "Password is required"
    }else if(values.password.length < 6){
        errors.password = "Password needs to be six characters or more"
    }

    if(!values.password2){
        errors.password2 = "Password is required"
    }else if(values.password !== values.password2){
        errors.password2 = "Passwords do not match"
    }

    return errors
}

The function takes in an object representing form inputs, in this case it accepts username, email, password and password2 for confirming password. A series of if statements check if the inputs are present and then performs some validation rules. If the inputs have errors, the name of the input and the error is appended to an object that is returned by the function.

Next we have the hook, which imports the validation function above and then performs the validation whenever the user clicks submit.

import React, { useState } from "react";
import validateInfo from "./validateinfo";

const useForm = () =>{
    const [values, setValues] = useState({
        username:"",
        email:"",
        password:"",
        password2:""
    })

    const [errors, setErrors] = useState<Partial<Ivalues>>({})
    const [isSubmitting, setIsSubmitting] = useState(false)


    const handlechange = (e:React.ChangeEvent<HTMLInputElement>) =>{
        const { name, value } = e.target

        setValues({...values, [name]: value}) 
    }

    const handleSubmit = (e:React.FormEvent)=>{
        e.preventDefault()
        setErrors(validateInfo(values))

        if(Object.keys(validateInfo(values)).length === 0){
            setIsSubmitting(true)
            try {
                // submit to backend
                console.log("SUBMITTED!!! ")
            } catch (error) {
                console.log("Could not submit form")
            } finally{
                setIsSubmitting(false)
            }
        }
    }

    return { handlechange, handleSubmit, values, errors, isSubmitting }
}

export default useForm

The hook, maintains the state for the form inputs and submits the form inputs for validation whenever handlesubmit is triggered when the user clicks the sign up button. The hook returns

  • handlechange: for updating state when form inputs change,

  • handlesubmit: triggered when the user submits the form, this function then passes the form inputs to the validating function and then stores any errors in the error state

  • Values: this is the state to hold the form inputs

  • errors: any errors in the form inputs are placed in this state, and the component that uses useForm hook can display the errors to the user

  • isSubmitting: this signals to components that the form is in a submitting state, so that the components can implement logic for loading indicators etc

Finally we have the component that displays the form to the user, this component imports the useForm hook. and uses output from the hook to display errors and submit the form

import useForm from "./useForm"


const FormSignup = () => {

    const { handleSubmit, handlechange, values, errors } = useForm()

  return (
    <div className="form-content-right">
        <form className="form">
            <h1>
                Get started with us today, create your account by filling the information
                below
            </h1>
            <div className="form-inputs">
                <label htmlFor="username" className="form-label"> User Name</label>
                <input 
                    id="username"
                    type="text" 
                    className="form-input"
                    name="username"
                    placeholder="Enter your username"
                    value={values.username}
                    onChange={handlechange}
                />
                {errors.username && <p>{errors.username}</p>}
            </div>
            <div className="form-inputs">
                <label htmlFor="email" className="form-label"> Email</label>
                <input 
                    id="email"
                    type="email" 
                    className="form-input"
                    name="email"
                    placeholder="Enter your email"
                    value={values.email}
                    onChange={handlechange}
                />
                {errors.email && <p>{errors.email}</p>}
            </div>
            <div className="form-inputs">
                <label htmlFor="password" className="form-label"> Password</label>
                <input 
                    id="password"
                    type="password" 
                    className="form-input"
                    name="password"
                    placeholder="Enter your password"
                    onChange={handlechange}
                    value={values.password}
                />
                {errors.password && <p>{errors.password}</p>}
            </div>
            <div className="form-inputs">
                <label htmlFor="password2" className="form-label"> Confirm Password</label>
                <input 
                    id="password2"
                    type="password2" 
                    className="form-input"
                    name="password2"
                    placeholder="Confirm password"
                    onChange={handlechange}
                    value={values.password2}
                />
                {errors.password2 && <p>{errors.password2}</p>}
            </div>
            <button 
                className="form-input-btn"
                type="submit"
                onClick={handleSubmit}
            >
                Sign Up
            </button>
            <span className="form-input-login">
                Already have an account? Login <a href="#">Here</a>
            </span>
        </form>
    </div>
  )
}

export default FormSignup

With a bit of css you can display the useful error message to users, powered by the custom hook!