Advanced React Hook Form: Custom Validation and Async Server Checks with Material UI
Advanced Form Validation in React Hook Form: Custom Rules & Async Server Validation
When building production web apps, basic field validations like required or standard regex patterns only cover half the story. Real-world enterprise forms frequently demand:
Cross-Field Synchronous Validation: Ensuring two fields match (like Password and Confirm Password, or Start Date and End Date).
Asynchronous Server-Side Checks: Validating input against a backend REST API in real time (e.g., verifying if an Employee ID or Email is already registered before submission).
In this tutorial, we will build on our previous guide on
1. Prerequisites & Setup
If you followed our previous guide, you already have @mui/material and react-hook-form installed.
If starting fresh, install the dependencies:
npm install react-hook-form @mui/material @emotion/react @emotion/styled
2. Implementing Custom Synchronous Validation (Cross-Field Matching)
React Hook Form provides a validate callback inside the registration rules. The function receives the current field's value and can either return true (valid) or a string error message (invalid).
To compare against another field (such as password confirmation), use the watch hook:
import React from 'react';
import { useForm, Controller } from 'react-hook-form';
import { TextField, Button, Box, Typography } from '@mui/material';
export default function SecurityForm() {
const {
control,
handleSubmit,
watch,
formState: { errors }
} = useForm({
defaultValues: {
password: '',
confirmPassword: ''
},
mode: 'onTouched'
});
// Watch the primary field to compare against
const passwordValue = watch('password');
const onSubmit = (data) => {
console.log('Form Submitted Successfully:', data);
};
return (
<Box component="form" onSubmit={handleSubmit(onSubmit)} sx={{ maxWidth: 400, mx: 'auto', mt: 4 }}>
<Typography variant="h6" gutterBottom>
Account Security Setup
</Typography>
{/* Password Field */}
<Controller
name="password"
control={control}
rules={{
required: 'Password is required',
minLength: {
value: 8,
message: 'Password must be at least 8 characters long'
}
}}
render={({ field }) => (
<TextField
{...field}
type="password"
label="New Password"
fullWidth
margin="normal"
error={!!errors.password}
helperText={errors.password?.message}
/>
)}
/>
{/* Confirm Password Field */}
<Controller
name="confirmPassword"
control={control}
rules={{
required: 'Please confirm your password',
validate: (value) =>
value === passwordValue || 'Passwords do not match'
}}
render={({ field }) => (
<TextField
{...field}
type="password"
label="Confirm Password"
fullWidth
margin="normal"
error={!!errors.confirmPassword}
helperText={errors.confirmPassword?.message}
/>
)}
/>
<Button type="submit" variant="contained" color="primary" fullWidth sx={{ mt: 2 }}>
Update Password
</Button>
</Box>
);
}
3. Implementing Asynchronous Server-Side Validation
A common requirement is checking uniqueness before the user clicks Submit—for example, verifying whether an Employee ID is already taken in the database.
The validate function in React Hook Form natively supports async/await. Returning a string indicates an invalid entry.
Simulating a Backend API Service
Here is a mock service representing a Spring Boot REST API endpoint (GET /api/v1/employees/check-id?employeeId=...):
// apiService.js
export const checkEmployeeIdAvailability = async (empId) => {
// Simulate network latency (500ms)
await new Promise((resolve) => setTimeout(resolve, 500));
const existingIds = ['EMP101', 'EMP102', 'EMP103'];
return !existingIds.includes(empId.toUpperCase());
};
Async Field Implementation with Loading State
import React from 'react';
import { useForm, Controller } from 'react-hook-form';
import { TextField, Button, Box, CircularProgress, InputAdornment } from '@mui/material';
import { checkEmployeeIdAvailability } from './apiService';
export default function AsyncEmployeeForm() {
const {
control,
handleSubmit,
formState: { errors, isValidating, isSubmitting }
} = useForm({
defaultValues: {
employeeId: ''
},
mode: 'onBlur' // Validate when user clicks away
});
const onSubmit = async (data) => {
console.log('Valid Employee Data:', data);
};
return (
<Box component="form" onSubmit={handleSubmit(onSubmit)} sx={{ maxWidth: 400, mx: 'auto', mt: 4 }}>
<Controller
name="employeeId"
control={control}
rules={{
required: 'Employee ID is mandatory',
pattern: {
value: /^EMP\d{3}$/i,
message: 'Format must be EMP followed by 3 digits (e.g., EMP104)'
},
// Asynchronous validation rule
validate: async (value) => {
const isAvailable = await checkEmployeeIdAvailability(value);
return isAvailable || 'This Employee ID is already registered in the system';
}
}}
render={({ field }) => (
<TextField
{...field}
label="Employee ID"
fullWidth
margin="normal"
error={!!errors.employeeId}
helperText={errors.employeeId?.message}
InputProps={{
endAdornment: isValidating ? (
<InputAdornment position="end">
<CircularProgress size={20} />
</InputAdornment>
) : null
}}
/>
)}
/>
<Button
type="submit"
variant="contained"
fullWidth
disabled={isSubmitting || isValidating}
sx={{ mt: 2 }}
>
Register Employee
</Button>
</Box>
);
}
4. Best Practices for Async Validation
Use
mode: 'onBlur'for Async Rules: Avoid running remote API checks on every keystroke (onChange). Settingmode: 'onBlur'ensures the API call triggers only after the user finishes typing and leaves the field.Surface Loading States Clearly: Use
isValidatingfromformStateto display a spinner inside the input field so users know the system is verifying their input.Disable Form Submission During Checks: Prevent accidental submissions while network requests are still resolving by disabling the submit button when
isValidatingis true.
Conclusion :
Combining custom synchronous rules with asynchronous validation functions gives you full control over complex validation pipelines while keeping form state lean and performant.
In our upcoming guide, we will connect this frontend form to a Spring Boot REST API using standard @Valid and global exception handling (MethodArgumentNotValidException).
Related Tutorials:
Frontend : Registration From With ReactBackend : SpringBoot Interview Questions

Comments
Post a Comment