Show errors on form inputs as users are entering information using the useReducer hook in react and typescript

·

5 min read

One of the ways to show validation errors on forms is as the users are entering information on input fields. As the users fill the inputs, they are made aware of the problems with their fields, even before they click submit, this makes sure that they are able to correct their mistakes immediately and can submit information with high confidence that they have provided all the right details in the required format.

This article discusses how to show the validation errors as the users are filling out the information using the useReducer hook in react.

First lets define the initial state in the reducer before the user begins entering any information

    const initial_state = {
        username:{
            value:"",
            error:""
        },
        email:{
            value:"",
            error:""
        },
        password:{
            value:"",
            error:""
        },
        password2:{
            value:"",
            error:""
        }
    }

   const FORM_ACTIONS={
        NAME :"NAME",
        EMAIL:"EMAIL",
        PASSWORD:"PASSWORD",
        PASSWORD2:"PASSWORD2"
    }


    type Initial_state_type = typeof initial_state
    type Action_Type = {
        type: keyof typeof FORM_ACTIONS,
        payload: string
    }

The initial state is an object with properties username, email, password and password2. As the user enters data in the forms, these properties will get populated with the data and any errors. Also included is the object FORM_ACTIONS which will be used when dispatching payloads to the reducer, based on the FORM_ACTION property the useReducer hook will update the state accordingly.

Next we have the validating function which takes in a FORM_ACTION property and a string representing user input. The function is then able to validate the string based on the form action. For instance, if the form action is EMAIL, the validating function will apply email input validation rules to the user input string to make sure its a valid email and then return an error if the string fails the validation rules.

const validator = (input:keyof typeof FORM_ACTIONS, value: string, password2?:string) =>{
        switch(input){
            case "EMAIL":{
                if(!/\S+@\S+\.\S+/.test(value)){
                    return "Email provided is invalid"
                }else{
                    return ""
                }
            }
            case "NAME":{
                if(value.length < 4){
                    return "Name is too short"
                }else{
                    return ""
                }
            }
            case "PASSWORD":{
                if(value.length < 4){
                    return "Password is needed"
                }else{
                    return ""
                }
            }
            case "PASSWORD2":{
                if(value !== password2){
                    return "Passwords do not match"
                }else{
                    return ""
                }
            }
            default:
                return "";
        }
    }

The validation rules can be as elaborate as the needs of the form require, here the function is simply enforcing length, whether passwords match and whether the email field passes a regex for email fields.

The next function is the usereducer function which is responsible for changing the state based on which FORM_ACTION it receives.

    const formReducer = (state:Initial_state_type, action:Action_Type): Initial_state_type =>{
        switch(action.type){
            case "EMAIL":
                return {
                    ... state,
                    email:{
                        value: action.payload,
                        error: validator(action.type, action.payload)
                    }
                }
            case "NAME":
                return {
                    ... state,
                    username:{
                        value: action.payload,
                        error: validator(action.type, action.payload)
                    }
                }
            case "PASSWORD":
                return {
                    ... state,
                    password:{
                        value: action.payload,
                        error: validator(action.type, action.payload)
                    }
                }
            case "PASSWORD2":
                return {
                    ... state,
                    password2:{
                        value: action.payload,
                        error: validator(action.type, action.payload, state.password.value)
                    }
                }
            default:
                return state
        }
    }

The reducer function takes in two arguments

  • Initial_state: this is the initial state we defined initialy,

  • action: this is an object containing two properties, the FORM_ACTION, which signals to the reducer what part of the state to change and payload which contains the data used to change the state.

Each user input is passed through the validator function and any errors saved in the state, the component can then check if there any errors in the errors field and display these to the user.

Bringing all these together, this is how the form input component looks

import { useReducer } from "react" 
    const initial_state = {
        username:{
            value:"",
            error:""
        },
        email:{
            value:"",
            error:""
        },
        password:{
            value:"",
            error:""
        },
        password2:{
            value:"",
            error:""
        }
    }

    const FORM_ACTIONS={
        NAME :"NAME",
        EMAIL:"EMAIL",
        PASSWORD:"PASSWORD",
        PASSWORD2:"PASSWORD2"
    }


    type Initial_state_type = typeof initial_state
    type Action_Type = {
        type: keyof typeof FORM_ACTIONS,
        payload: string
    }


const FormUsingUseReducerValidation = () => {

    const validator = (input:keyof typeof FORM_ACTIONS, value: string, password2?:string) =>{
        switch(input){
            case "EMAIL":{
                if(!/\S+@\S+\.\S+/.test(value)){
                    return "Email provided is invalid"
                }else{
                    return ""
                }
            }
            case "NAME":{
                if(value.length < 4){
                    return "Name is too short"
                }else{
                    return ""
                }
            }
            case "PASSWORD":{
                if(value.length < 4){
                    return "Password is needed"
                }else{
                    return ""
                }
            }
            case "PASSWORD2":{
                if(value !== password2){
                    return "Passwords do not match"
                }else{
                    return ""
                }
            }
            default:
                return "";
        }
    }


    const formReducer = (state:Initial_state_type, action:Action_Type): Initial_state_type =>{
        switch(action.type){
            case "EMAIL":
                return {
                    ... state,
                    email:{
                        value: action.payload,
                        error: validator(action.type, action.payload)
                    }
                }
            case "NAME":
                return {
                    ... state,
                    username:{
                        value: action.payload,
                        error: validator(action.type, action.payload)
                    }
                }
            case "PASSWORD":
                return {
                    ... state,
                    password:{
                        value: action.payload,
                        error: validator(action.type, action.payload)
                    }
                }
            case "PASSWORD2":
                return {
                    ... state,
                    password2:{
                        value: action.payload,
                        error: validator(action.type, action.payload, state.password.value)
                    }
                }
            default:
                return state
        }
    }


    const [formState, dispatch] = useReducer(formReducer, initial_state);

  return (
    <div className="form-content-right">
        <form className="form">
            <h1>
                This form is using usereducer for validation
            </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={formState.username.value}
                    onChange={(e)=>{
                        dispatch({type:"NAME", payload: e.target.value})
                    }}
                />
                {formState.username.error && <p>{formState.username.error}</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={formState.email.value}
                    onChange={(e)=>{
                        dispatch({type:"EMAIL", payload: e.target.value})
                    }}
                />
                {formState.email.error && <p>{formState.email.error}</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"
                    value={formState.password.value}
                    onChange={(e)=>{
                        dispatch({type:"PASSWORD", payload: e.target.value})
                    }}
                />
                {formState.password.error && <p>{formState.password.error}</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"
                    value={formState.password2.value}
                    onChange={(e)=>{
                        dispatch({type:"PASSWORD2", payload: e.target.value})
                    }}
                />
                {formState.password2.error && <p>{formState.password2.error}</p>}
            </div>
            <button 
                className="form-input-btn"
                type="submit"
                onClick={()=>{}}
            >
                Sign Up
            </button>
            <span className="form-input-login">
                Already have an account? Login <a href="#">Here</a>
            </span>
        </form>
    </div>
  )
}

export default FormUsingUseReducerValidation

With this setup, users are able to see validation errors as they are typing in the inputs, which is more user friendly.