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.
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"
}
}