Reactjs form validation with useReducer hook

Last updated : Jul 30, 2023 12:00 AM

1. Overview

useReducer hook can be used to handle complex state management scenarios. Using regular state management becomes cumbersome when part of the state depends on other state values. useReducer simplify such state management by providing safe ways to access updated state values. This tutorial guides you through validating and submitting a form in Reactjs.

2. useReducer project scope

The code example shown is a component of a Reactjs project. You can use this code in an existing project or follow Reactjs official site to create a new Reactjs project.

3. form validation and submission code

Below is the complete code to validate and submit a form using the useReducer hook. The code is written in Typescript. If you are unfamiliar with Typescript, a Javascript version is also available.

Validate and submit a form using the useReducerDescription
import { useReducer } from "react"

type FormState = {
    firstName: string
    lastName: string
    age: string
    email: string
    password: string
}
const initialState: FormState = {
    firstName: "",
    lastName: "",
    age: "",
    email: "",
    password: ""
}
type FormValidityState = {
    firstNameError: boolean
    lastNameError: boolean
    ageError: boolean
    emailError: boolean
    passwordError: boolean
    isFormValid: boolean
}
const initialValidityState: FormValidityState = {
    firstNameError: false,
    lastNameError: false,
    ageError: false,
    emailError: false,
    passwordError: false,
    isFormValid: false
}
type FormAction = {
    type: string
    payLoad: string
}
type FormValidityAction = {
    type: string
    payLoad: FormState
}
const formReducer = (state: FormState, action: FormAction): FormState => {
    switch(action.type){
        case "UPDATE_FIRST_NAME": return{
            ...state, firstName: action.payLoad, 
        }
        case "UPDATE_LAST_NAME": return{
            ...state,lastName: action.payLoad, 
        }
        case "UPDATE_AGE": return{
            ...state, age: action.payLoad, 
        }
        case "UPDATE_EMAIL": return{
            ...state, email: action.payLoad, 
        }
        case "UPDATE_PASSWORD": return{
            ...state, password: action.payLoad, 
        }
        default:
            return state
    }
}
const formValidityReducer = (state: FormValidityState, action: FormValidityAction): FormValidityState => {
    let isValid: boolean = false;
    switch(action.type){
        case "VALIDATE_FIRST_NAME": 
        isValid = action.payLoad.firstName.length > 0 ? true: false
        return{
            ...state,
            ...({firstNameError: !isValid, isFormValid: isValid && !state.lastNameError && !state.ageError && !state.emailError && !state.passwordError}),
        }
        case "VALIDATE_LAST_NAME": 
        isValid = action.payLoad.lastName.length > 0 ? true: false
        return{
            ...state,
            ...({lastNameError: !isValid, isFormValid: isValid && !state.firstNameError && !state.ageError && !state.emailError && !state.passwordError})
        }
        case "VALIDATE_AGE": 
        isValid = action.payLoad.age.length > 0 ? true: false
        return{
            ...state,
            ...({ageError: !isValid, isFormValid: isValid && !state.firstNameError && !state.lastNameError && !state.emailError && !state.passwordError})
        }
        case "VALIDATE_EMAIL": 
        isValid = (action.payLoad.email.length > 0 && action.payLoad.email.includes("@")) ? true: false
        return{
            ...state,
            ...({emailError: !isValid, isFormValid: isValid && !state.firstNameError && !state.lastNameError && !state.ageError && !state.passwordError})
        }
        case "VALIDATE_PASSWORD": 
        isValid = action.payLoad.password.length > 9 ? true: false
        return{
            ...state,
            ...({passwordError: !isValid, isFormValid: isValid && !state.firstNameError && !state.lastNameError && !state.ageError && !state.emailError})
        }
        default:
        return state
    }
}

export const Form = () => {
    const [formData, setFormData] = useReducer(formReducer, initialState)
    const [formValidityData, setFormValidityData] = useReducer(formValidityReducer, initialValidityState)

    const onButtonPress = (event: React.FormEvent<HTMLFormElement>) => {
        event.preventDefault()
        console.log(formData)
        //Form submission happens here
    }
    return(
        <div style={STYLE.container}>
          <form onSubmit={onButtonPress}>
              <label style={STYLE.formElement} htmlFor="first_name">First Name</label>
              <div style={STYLE.formElement}>
                  <input 
                  id="first_name" 
                  style={{backgroundColor:formValidityData.firstNameError ?"pink" : ""}} 
                  onChange={(e) =>setFormData({type:"UPDATE_FIRST_NAME", payLoad:e.target.value})}
                  onBlur={(e) => setFormValidityData({type: "VALIDATE_FIRST_NAME", payLoad: formData})}
                  type="text"/>
              </div>
             <label style={STYLE.formElement} htmlFor="last_name">Last Name</label>
              <div style={STYLE.formElement}>
                  <input 
                  id="last_name" 
                  style={{backgroundColor:formValidityData.lastNameError ? "pink" : ""}} 
                  onChange={(e) =>setFormData({type:"UPDATE_LAST_NAME", payLoad:e.target.value})}
                  onBlur={(e) => setFormValidityData({type: "VALIDATE_LAST_NAME", payLoad: formData})}
                  type="text"/>
              </div>
              <label style={STYLE.formElement} htmlFor="last_name">Email</label>
              <div style={STYLE.formElement}>
                  <input 
                  id="email" 
                  style={{backgroundColor:formValidityData.emailError ? "pink" : ""}} 
                  onChange={(e) =>setFormData({type:"UPDATE_EMAIL", payLoad:e.target.value})}
                  onBlur={(e) => setFormValidityData({type: "VALIDATE_EMAIL", payLoad: formData})}
                  type="text"/>
              </div>
              <label style={STYLE.formElement} htmlFor="last_name">Password</label>
              <div style={STYLE.formElement}>
                  <input 
                  id="password" 
                  style={{backgroundColor:formValidityData.passwordError ? "pink" : ""}} 
                  onChange={(e) =>setFormData({type:"UPDATE_PASSWORD", payLoad:e.target.value})}
                  onBlur={(e) => setFormValidityData({type: "VALIDATE_PASSWORD", payLoad: formData})}
                  type="password"/>
              </div>
             <label style={STYLE.formElement} htmlFor="age">Age</label>
              <div style={STYLE.formElement}>
                  <input 
                  id="age" 
                  style={{backgroundColor:formValidityData.ageError ? "pink" : ""}} 
                  onChange={(e) =>setFormData({type:"UPDATE_AGE", payLoad:e.target.value})} 
                  onBlur={(e) => setFormValidityData({type: "VALIDATE_AGE", payLoad: formData})}
                  type="number"/>
              </div>
              <div style={STYLE.formElement}>
                  <input disabled={!formValidityData.isFormValid} type="submit" value={""+formValidityData.isFormValid}/>
              </div>
          </form>
        </div>
    )
}
const STYLE = {
    container: {
        borderRadius: "5px",
        backgroundColor: "#f2f2f2",
        padding: "20px",
        maxWidth:"240px"
    },
    formElement: {
        padding: "6px 24px"
    }
}

4. Form validation

4.1 Form validation with useReducer

Note that in each validation case, we depend on multiple state variables. For example, to validate the firstName, we need access to four different state variables. When using the state in useReducer, the state object is guaranteed to be up to date.

const formValidityReducer = (state: FormValidityState, action: FormValidityAction): FormValidityState => {
    let isValid: boolean = false;
    switch(action.type){
        case "VALIDATE_FIRST_NAME": 
        isValid = action.payLoad.firstName.length > 0 ? true: false
        return{
            ...state,
            ...({firstNameError: !isValid, isFormValid: isValid && !state.lastNameError && !state.ageError && !state.emailError && !state.passwordError}),
        }
        case "VALIDATE_LAST_NAME": 
        isValid = action.payLoad.lastName.length > 0 ? true: false
        return{
            ...state,
            ...({lastNameError: !isValid, isFormValid: isValid && !state.firstNameError && !state.ageError && !state.emailError && !state.passwordError})
        }
        case "VALIDATE_AGE": 
        isValid = action.payLoad.age.length > 0 ? true: false
        return{
            ...state,
            ...({ageError: !isValid, isFormValid: isValid && !state.firstNameError && !state.lastNameError && !state.emailError && !state.passwordError})
        }
        case "VALIDATE_EMAIL": 
        isValid = (action.payLoad.email.length > 0 && action.payLoad.email.includes("@")) ? true: false
        return{
            ...state,
            ...({emailError: !isValid, isFormValid: isValid && !state.firstNameError && !state.lastNameError && !state.ageError && !state.passwordError})
        }
        case "VALIDATE_PASSWORD": 
        isValid = action.payLoad.password.length > 9 ? true: false
        return{
            ...state,
            ...({passwordError: !isValid, isFormValid: isValid && !state.firstNameError && !state.lastNameError && !state.ageError && !state.emailError})
        }
        default:
        return state
    }
}

4.2 Important points to notice

In each case statement, we use the newly created isValid variable. Because it is unsafe to access the freshly updated state before the async call is complete. For example, you cannot access the state.firstNameError in the same case statement where we update it. The state update is asynchronous. The useReducer hook guarantees safer access to the previous state, not to state changes happen with the current state update.

5. Form state

5.1 Maintaining Form state with useReducer

const formReducer = (state: FormState, action: FormAction): FormState => {
    switch(action.type){
        case "UPDATE_FIRST_NAME": return{
            ...state, firstName: action.payLoad, 
        }
        case "UPDATE_LAST_NAME": return{
            ...state,lastName: action.payLoad, 
        }
        case "UPDATE_AGE": return{
            ...state, age: action.payLoad, 
        }
        case "UPDATE_EMAIL": return{
            ...state, email: action.payLoad, 
        }
        case "UPDATE_PASSWORD": return{
            ...state, password: action.payLoad, 
        }
        default:
            return state
    }
}

5.2 Separate state for form state and validation state

We use two state objects to keep form data separate from validation data. You can achieve the same result with a single state object and filter the validation properties before submitting the form, keeping the response clean.

6. Javascript version of useReducer form validation and submission

import { useReducer } from "react"

const initialState = {
    firstName: "",
    lastName: "",
    age: "",
    email: "",
    password: ""
}
const initialValidityState = {
    firstNameError: false,
    lastNameError: false,
    ageError: false,
    emailError: false,
    passwordError: false,
    isFormValid: false
}
const formReducer = (state, action) => {
    const {name, value} = action.type
    return{
        ...state, [name]: value, 
    }
}
const formValidityReducer = (state, action) => {
    let isValid = false;
    switch(action.type){
        case "VALIDATE_FIRST_NAME": 
        isValid = action.payLoad.firstName.length > 0 ? true: false
        return{
            ...state,
            ...({firstNameError: !isValid, isFormValid: isValid && !state.lastNameError && !state.ageError && !state.emailError && !state.passwordError}),
        }
        case "VALIDATE_LAST_NAME": 
        isValid = action.payLoad.lastName.length > 0 ? true: false
        return{
            ...state,
            ...({lastNameError: !isValid, isFormValid: isValid && !state.firstNameError && !state.ageError && !state.emailError && !state.passwordError})
        }
        case "VALIDATE_AGE": 
        isValid = action.payLoad.age.length > 0 ? true: false
        return{
            ...state,
            ...({ageError: !isValid, isFormValid: isValid && !state.firstNameError && !state.lastNameError && !state.emailError && !state.passwordError})
        }
        case "VALIDATE_EMAIL": 
        isValid = (action.payLoad.email.length > 0 && action.payLoad.email.includes("@")) ? true: false
        return{
            ...state,
            ...({emailError: !isValid, isFormValid: isValid && !state.firstNameError && !state.lastNameError && !state.ageError && !state.passwordError})
        }
        case "VALIDATE_PASSWORD": 
        isValid = action.payLoad.password.length > 9 ? true: false
        return{
            ...state,
            ...({passwordError: !isValid, isFormValid: isValid && !state.firstNameError && !state.lastNameError && !state.ageError && !state.emailError})
        }
        default:
        return state
    }
}
export const Form = () => {

    const [formData, setFormData] = useReducer(formReducer, initialState)
    const [formValidityData, setFormValidityData] = useReducer(formValidityReducer, initialValidityState)

    const onButtonPress = (event) => {
        event.preventDefault()
        console.log(formData)
        //Form submission happens here
    }
    return(
        <div style={STYLE.container}>
          <form onSubmit={onButtonPress}>
              <label style={STYLE.formElement} htmlFor="first_name">First Name</label>
              <div style={STYLE.formElement}>
                  <input 
                  name="firstName" 
                  style={{backgroundColor:formValidityData.firstNameError ?"pink" : ""}} 
                  onChange={(e) =>setFormData({type:e.target})}
                  onBlur={(e) => setFormValidityData({type: "VALIDATE_FIRST_NAME", payLoad: formData})}
                  type="text"/>
              </div>
             <label style={STYLE.formElement} htmlFor="last_name">Last Name</label>
              <div style={STYLE.formElement}>
                  <input 
                  name="lastName" 
                  style={{backgroundColor:formValidityData.lastNameError ? "pink" : ""}} 
                  onChange={(e) =>setFormData({type:e.target})}
                  onBlur={(e) => setFormValidityData({type: "VALIDATE_LAST_NAME", payLoad: formData})}
                  type="text"/>
              </div>
              <label style={STYLE.formElement} htmlFor="last_name">Email</label>
              <div style={STYLE.formElement}>
                  <input 
                  name="email" 
                  style={{backgroundColor:formValidityData.emailError ? "pink" : ""}} 
                  onChange={(e) =>setFormData({type:e.target})}
                  onBlur={(e) => setFormValidityData({type: "VALIDATE_EMAIL", payLoad: formData})}
                  type="text"/>
              </div>
              <label style={STYLE.formElement} htmlFor="last_name">Password</label>
              <div style={STYLE.formElement}>
                  <input 
                  name="password" 
                  style={{backgroundColor:formValidityData.passwordError ? "pink" : ""}} 
                  onChange={(e) =>setFormData({type:e.target})}
                  onBlur={(e) => setFormValidityData({type: "VALIDATE_PASSWORD", payLoad: formData})}
                  type="password"/>
              </div>
              <label style={STYLE.formElement} htmlFor="age">Age</label>
              <div style={STYLE.formElement}>
                  <input 
                  name="age" 
                  style={{backgroundColor:formValidityData.ageError ? "pink" : ""}} 
                  onChange={(e) =>setFormData({type:e.target})} 
                  onBlur={(e) => setFormValidityData({type: "VALIDATE_AGE", payLoad: formData})}
                  type="number"/>
              </div>
              <div style={STYLE.formElement}>
                  <input disabled={!formValidityData.isFormValid} type="submit" value={""+formValidityData.isFormValid}/>
              </div>
          </form>
        </div>
    )
}
const STYLE = {
    container: {
        borderRadius: "5px",
        backgroundColor: "#f2f2f2",
        padding: "20px",
        maxWidth:"240px"
    },
    formElement: {
        padding: "6px 24px"
    }
}
Lance

By: Lance

Hi, I'm Lance Raney, a dedicated Fullstack Developer based in Oklahoma with over 15 years of exp

Read more...