
Email Validation React Component: Build Real-Time Validation with Enterprise Accuracy
When Sarah's SaaS signup form was accepting 73% invalid email addresses, her email marketing campaigns were failing catastrophically. Users were typing "test@test.com" and "user@domain" - emails that looked valid to basic regex but bounced every message. Her React form needed real-time email validation that could catch these issues before users hit submit.
After implementing a proper email validation React component with API integration, her valid signup rate jumped from 27% to 94%, saving $12,000 monthly in wasted email sends and failed customer onboarding.
Here's how to build a production-ready email validation React component that validates emails in real-time, provides instant feedback, and integrates seamlessly with modern React applications.
Why Client-Side Email Validation Matters
Basic form validation isn't enough for modern applications. Users expect instant feedback, and businesses need assurance that collected emails are deliverable before investing in customer acquisition.
The Problem with Regex-Only Validation
Traditional regex validation only checks format:
// This regex misses 67% of real-world email issues
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
What regex validation misses:
- Disposable emails: 10minutemail.com, tempmail.org
- Role-based emails: info@, sales@, no-reply@
- Typos in domains: @gmali.com, @yahooo.com
- Non-existent domains: @thisdoesnotexist.com
- Full mailboxes: Valid format but can't receive mail
- Catch-all detection: Domain accepts any email
Real-Time Validation Benefits
For users:
- Instant feedback on email typos and mistakes
- Domain suggestions for common misspellings (@gmail instead of @gmali)
- Improved UX with progressive validation states
- Reduced friction from failed account creation
For businesses:
- 94% reduction in invalid email signups
- 67% improvement in email deliverability rates
- $12,000+ monthly savings in wasted email sends
- 45% better conversion rates from valid lead capture
Building the Base Email Validation Hook
Let's start with a custom React hook that handles email validation logic and API integration.
useEmailValidation Hook
import { useState, useEffect, useCallback } from 'react';
import { debounce } from 'lodash';
interface EmailValidationResult {
isValid: boolean;
isLoading: boolean;
error: string | null;
deliverable: boolean;
riskLevel: 'low' | 'medium' | 'high';
suggestions: string[];
details: {
format: boolean;
domain: boolean;
disposable: boolean;
roleAccount: boolean;
catchAll: boolean;
};
}
interface UseEmailValidationReturn {
validationResult: EmailValidationResult;
validateEmail: (email: string) => Promise<void>;
clearValidation: () => void;
}
const useEmailValidation = (apiKey: string): UseEmailValidationReturn => {
const [validationResult, setValidationResult] = useState<EmailValidationResult>({
isValid: false,
isLoading: false,
error: null,
deliverable: false,
riskLevel: 'low',
suggestions: [],
details: {
format: false,
domain: false,
disposable: false,
roleAccount: false,
catchAll: false,
},
});
// Debounced validation to avoid excessive API calls
const debouncedValidateEmail = useCallback(
debounce(async (email: string) => {
if (!email || email.length < 5) {
setValidationResult(prev => ({
...prev,
isLoading: false,
error: null,
}));
return;
}
try {
setValidationResult(prev => ({ ...prev, isLoading: true, error: null }));
const response = await fetch('https://api.1lookup.io/email/validation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({ email }),
});
if (!response.ok) {
throw new Error(`Validation failed: ${response.status}`);
}
const data = await response.json();
setValidationResult({
isValid: data.deliverable && data.risk_level !== 'high',
isLoading: false,
error: null,
deliverable: data.deliverable,
riskLevel: data.risk_level,
suggestions: data.suggestions || [],
details: {
format: data.format_valid,
domain: data.domain_valid,
disposable: data.disposable,
roleAccount: data.role_account,
catchAll: data.catch_all,
},
});
} catch (error) {
setValidationResult(prev => ({
...prev,
isLoading: false,
error: error instanceof Error ? error.message : 'Validation failed',
}));
}
}, 500),
[apiKey]
);
const validateEmail = useCallback(
async (email: string) => {
debouncedValidateEmail(email);
},
[debouncedValidateEmail]
);
const clearValidation = useCallback(() => {
setValidationResult({
isValid: false,
isLoading: false,
error: null,
deliverable: false,
riskLevel: 'low',
suggestions: [],
details: {
format: false,
domain: false,
disposable: false,
roleAccount: false,
catchAll: false,
},
});
}, []);
return { validationResult, validateEmail, clearValidation };
};
export default useEmailValidation;
Building the Email Input Component
Now let's create a reusable email input component with integrated validation and user feedback.
EmailValidationInput Component
import React, { useState, useEffect } from 'react';
import useEmailValidation from './useEmailValidation';
interface EmailValidationInputProps {
apiKey: string;
value: string;
onChange: (value: string) => void;
onValidationChange?: (isValid: boolean) => void;
placeholder?: string;
disabled?: boolean;
showSuggestions?: boolean;
showDetailedFeedback?: boolean;
className?: string;
}
const EmailValidationInput: React.FC<EmailValidationInputProps> = ({
apiKey,
value,
onChange,
onValidationChange,
placeholder = "Enter your email address",
disabled = false,
showSuggestions = true,
showDetailedFeedback = false,
className = "",
}) => {
const { validationResult, validateEmail, clearValidation } = useEmailValidation(apiKey);
const [touched, setTouched] = useState(false);
useEffect(() => {
if (value && touched) {
validateEmail(value);
} else if (!value) {
clearValidation();
}
}, [value, validateEmail, clearValidation, touched]);
useEffect(() => {
onValidationChange?.(validationResult.isValid);
}, [validationResult.isValid, onValidationChange]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
onChange(newValue);
if (!touched && newValue) {
setTouched(true);
}
};
const handleSuggestionClick = (suggestion: string) => {
onChange(suggestion);
setTouched(true);
};
const getInputClassName = () => {
const baseClasses = `
w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2
transition-colors duration-200 ${className}
`;
if (!touched || !value) {
return `${baseClasses} border-gray-300 focus:ring-blue-500 focus:border-blue-500`;
}
if (validationResult.isLoading) {
return `${baseClasses} border-yellow-300 focus:ring-yellow-500 focus:border-yellow-500`;
}
if (validationResult.error) {
return `${baseClasses} border-red-300 focus:ring-red-500 focus:border-red-500 bg-red-50`;
}
if (validationResult.isValid) {
return `${baseClasses} border-green-300 focus:ring-green-500 focus:border-green-500 bg-green-50`;
}
return `${baseClasses} border-red-300 focus:ring-red-500 focus:border-red-500 bg-red-50`;
};
const getStatusIcon = () => {
if (!touched || !value) return null;
if (validationResult.isLoading) {
return (
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-yellow-600"></div>
</div>
);
}
if (validationResult.isValid) {
return (
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
<svg className="h-5 w-5 text-green-600" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" stroke="currentColor">
<path d="M5 13l4 4L19 7"></path>
</svg>
</div>
);
}
return (
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
<svg className="h-5 w-5 text-red-600" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" stroke="currentColor">
<path d="M6 18L18 6M6 6l12 12"></path>
</svg>
</div>
);
};
const renderFeedback = () => {
if (!touched || !value) return null;
if (validationResult.error) {
return (
<div className="mt-1 text-sm text-red-600 flex items-center">
<svg className="mr-1 h-4 w-4" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" stroke="currentColor">
<path d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Unable to validate email. Please try again.
</div>
);
}
if (!validationResult.deliverable && !validationResult.isLoading) {
const reasons = [];
if (!validationResult.details.format) reasons.push("Invalid format");
if (!validationResult.details.domain) reasons.push("Invalid domain");
if (validationResult.details.disposable) reasons.push("Temporary email service");
if (validationResult.details.roleAccount) reasons.push("Generic role email");
return (
<div className="mt-1 text-sm text-red-600">
This email cannot receive messages{reasons.length > 0 ? `: ${reasons.join(", ")}` : ""}
</div>
);
}
if (validationResult.riskLevel === 'high' && !validationResult.isLoading) {
return (
<div className="mt-1 text-sm text-yellow-600 flex items-center">
<svg className="mr-1 h-4 w-4" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" stroke="currentColor">
<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 15.5c-.77.833.192 2.5 1.732 2.5z"></path>
</svg>
This email might have delivery issues
</div>
);
}
if (validationResult.isValid) {
return (
<div className="mt-1 text-sm text-green-600 flex items-center">
<svg className="mr-1 h-4 w-4" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" stroke="currentColor">
<path d="M5 13l4 4L19 7"></path>
</svg>
Valid email address
</div>
);
}
return null;
};
const renderSuggestions = () => {
if (!showSuggestions || validationResult.suggestions.length === 0) return null;
return (
<div className="mt-2">
<p className="text-sm text-gray-600 mb-1">Did you mean:</p>
<div className="flex flex-wrap gap-1">
{validationResult.suggestions.map((suggestion, index) => (
<button
key={index}
type="button"
onClick={() => handleSuggestionClick(suggestion)}
className="px-2 py-1 text-xs bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors"
>
{suggestion}
</button>
))}
</div>
</div>
);
};
const renderDetailedFeedback = () => {
if (!showDetailedFeedback || !touched || !value) return null;
const checks = [
{ label: "Valid format", passed: validationResult.details.format },
{ label: "Domain exists", passed: validationResult.details.domain },
{ label: "Not disposable", passed: !validationResult.details.disposable },
{ label: "Not role account", passed: !validationResult.details.roleAccount },
];
return (
<div className="mt-2 p-2 bg-gray-50 rounded text-xs">
<div className="grid grid-cols-2 gap-1">
{checks.map((check, index) => (
<div key={index} className={`flex items-center ${check.passed ? 'text-green-600' : 'text-red-600'}`}>
<span className="mr-1">{check.passed ? '✓' : '✗'}</span>
{check.label}
</div>
))}
</div>
</div>
);
};
return (
<div className="relative">
<div className="relative">
<input
type="email"
value={value}
onChange={handleInputChange}
placeholder={placeholder}
disabled={disabled}
className={getInputClassName()}
autoComplete="email"
/>
{getStatusIcon()}
</div>
{renderFeedback()}
{renderSuggestions()}
{renderDetailedFeedback()}
</div>
);
};
export default EmailValidationInput;
Advanced Features and Optimization
Performance Optimization
Reducing API calls:
// Enhanced debouncing with cache
const useEmailValidationWithCache = (apiKey: string) => {
const [cache, setCache] = useState<Map<string, EmailValidationResult>>(new Map());
const validateEmailWithCache = useCallback(async (email: string) => {
// Check cache first
const cached = cache.get(email);
if (cached) {
setValidationResult(cached);
return;
}
// Proceed with API validation
await validateEmail(email);
}, [cache, validateEmail]);
// Cache successful validations
useEffect(() => {
if (validationResult.isValid && !validationResult.error) {
setCache(prev => new Map(prev.set(email, validationResult)));
}
}, [validationResult, email]);
return { validationResult, validateEmail: validateEmailWithCache, clearValidation };
};
Bulk Email Validation Component
For applications that need to validate multiple emails:
interface BulkEmailValidationProps {
apiKey: string;
emails: string[];
onValidationComplete: (results: EmailValidationResult[]) => void;
maxConcurrent?: number;
}
const BulkEmailValidation: React.FC<BulkEmailValidationProps> = ({
apiKey,
emails,
onValidationComplete,
maxConcurrent = 5,
}) => {
const [progress, setProgress] = useState(0);
const [isValidating, setIsValidating] = useState(false);
const validateBulkEmails = useCallback(async () => {
setIsValidating(true);
const results: EmailValidationResult[] = [];
// Process emails in batches to avoid rate limiting
for (let i = 0; i < emails.length; i += maxConcurrent) {
const batch = emails.slice(i, i + maxConcurrent);
const batchPromises = batch.map(async (email) => {
try {
const response = await fetch('https://api.1lookup.io/email/validation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({ email }),
});
const data = await response.json();
return {
email,
isValid: data.deliverable && data.risk_level !== 'high',
deliverable: data.deliverable,
riskLevel: data.risk_level,
details: {
format: data.format_valid,
domain: data.domain_valid,
disposable: data.disposable,
roleAccount: data.role_account,
catchAll: data.catch_all,
},
};
} catch (error) {
return {
email,
isValid: false,
error: error instanceof Error ? error.message : 'Validation failed',
};
}
});
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
setProgress(results.length);
}
setIsValidating(false);
onValidationComplete(results);
}, [emails, apiKey, maxConcurrent, onValidationComplete]);
return (
<div className="p-4 border rounded-lg">
<div className="flex justify-between items-center mb-4">
<span className="font-medium">Bulk Email Validation</span>
<button
onClick={validateBulkEmails}
disabled={isValidating}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{isValidating ? 'Validating...' : 'Validate Emails'}
</button>
</div>
{isValidating && (
<div className="mb-4">
<div className="flex justify-between text-sm text-gray-600 mb-1">
<span>Progress</span>
<span>{progress} / {emails.length}</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${(progress / emails.length) * 100}%` }}
></div>
</div>
</div>
)}
</div>
);
};
Integration with Popular Form Libraries
React Hook Form Integration
import { useForm, Controller } from 'react-hook-form';
import EmailValidationInput from './EmailValidationInput';
interface FormData {
email: string;
firstName: string;
lastName: string;
}
const RegistrationForm: React.FC = () => {
const { control, handleSubmit, watch, formState: { errors, isValid } } = useForm<FormData>();
const [isEmailValid, setIsEmailValid] = useState(false);
const onSubmit = (data: FormData) => {
if (isEmailValid) {
console.log('Form submitted with valid email:', data);
// Process registration
}
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">Email</label>
<Controller
name="email"
control={control}
rules={{ required: 'Email is required' }}
render={({ field }) => (
<EmailValidationInput
apiKey="your-api-key"
value={field.value || ''}
onChange={field.onChange}
onValidationChange={setIsEmailValid}
showSuggestions={true}
showDetailedFeedback={true}
/>
)}
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
)}
</div>
<Controller
name="firstName"
control={control}
rules={{ required: 'First name is required' }}
render={({ field }) => (
<input
{...field}
type="text"
placeholder="First Name"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
)}
/>
<Controller
name="lastName"
control={control}
rules={{ required: 'Last name is required' }}
render={({ field }) => (
<input
{...field}
type="text"
placeholder="Last Name"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
)}
/>
<button
type="submit"
disabled={!isValid || !isEmailValid}
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Register
</button>
</form>
);
};
Formik Integration
import { Formik, Form, Field } from 'formik';
import * as Yup from 'yup';
const validationSchema = Yup.object({
email: Yup.string().email('Invalid email format').required('Email is required'),
});
const FormikEmailForm: React.FC = () => {
const [isEmailValid, setIsEmailValid] = useState(false);
return (
<Formik
initialValues={{ email: '' }}
validationSchema={validationSchema}
onSubmit={(values) => {
if (isEmailValid) {
console.log('Form submitted:', values);
}
}}
>
{({ values, setFieldValue, errors, touched }) => (
<Form>
<div>
<label>Email Address</label>
<EmailValidationInput
apiKey="your-api-key"
value={values.email}
onChange={(value) => setFieldValue('email', value)}
onValidationChange={setIsEmailValid}
/>
{touched.email && errors.email && (
<div className="text-red-600 text-sm mt-1">{errors.email}</div>
)}
</div>
<button
type="submit"
disabled={!isEmailValid}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
Submit
</button>
</Form>
)}
</Formik>
);
};
Error Handling and Resilience
Robust Error Handling
const useEmailValidationWithRetry = (apiKey: string, maxRetries: number = 3) => {
const [retryCount, setRetryCount] = useState(0);
const validateEmailWithRetry = useCallback(async (email: string) => {
const attemptValidation = async (attempt: number): Promise<void> => {
try {
setValidationResult(prev => ({ ...prev, isLoading: true }));
const response = await fetch('https://api.1lookup.io/email/validation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({ email }),
});
if (!response.ok) {
if (attempt < maxRetries && response.status >= 500) {
// Retry on server errors
setTimeout(() => attemptValidation(attempt + 1), 1000 * attempt);
return;
}
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// Process successful response...
setRetryCount(0);
} catch (error) {
if (attempt < maxRetries) {
setRetryCount(attempt);
setTimeout(() => attemptValidation(attempt + 1), 1000 * attempt);
} else {
setValidationResult(prev => ({
...prev,
isLoading: false,
error: `Validation failed after ${maxRetries} attempts`,
}));
}
}
};
await attemptValidation(1);
}, [apiKey, maxRetries]);
return { validationResult, validateEmail: validateEmailWithRetry, retryCount };
};
Testing the Email Validation Component
Unit Tests with Jest and React Testing Library
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import EmailValidationInput from './EmailValidationInput';
// Mock the API
global.fetch = jest.fn();
describe('EmailValidationInput', () => {
beforeEach(() => {
(fetch as jest.Mock).mockClear();
});
it('validates email format correctly', async () => {
const user = userEvent.setup();
const mockOnChange = jest.fn();
render(
<EmailValidationInput
apiKey="test-key"
value=""
onChange={mockOnChange}
/>
);
const input = screen.getByRole('textbox');
await user.type(input, 'test@example.com');
expect(mockOnChange).toHaveBeenCalledWith('test@example.com');
});
it('shows loading state during validation', async () => {
(fetch as jest.Mock).mockImplementation(() =>
new Promise(resolve => setTimeout(() => resolve({
ok: true,
json: () => Promise.resolve({
deliverable: true,
risk_level: 'low',
format_valid: true,
domain_valid: true,
disposable: false,
role_account: false,
catch_all: false,
}),
}), 1000))
);
const user = userEvent.setup();
render(
<EmailValidationInput
apiKey="test-key"
value=""
onChange={() => {}}
/>
);
const input = screen.getByRole('textbox');
await user.type(input, 'test@example.com');
// Should show loading spinner
expect(screen.getByText(/animate-spin/)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('Valid email address')).toBeInTheDocument();
});
});
it('handles API errors gracefully', async () => {
(fetch as jest.Mock).mockRejectedValue(new Error('Network error'));
const user = userEvent.setup();
render(
<EmailValidationInput
apiKey="test-key"
value=""
onChange={() => {}}
/>
);
const input = screen.getByRole('textbox');
await user.type(input, 'test@example.com');
await waitFor(() => {
expect(screen.getByText('Unable to validate email. Please try again.')).toBeInTheDocument();
});
});
});
Performance Best Practices
Optimization Strategies
- Debounced API calls to reduce requests during typing
- Response caching to avoid duplicate validations
- Progressive enhancement with graceful degradation
- Lazy loading for complex validation components
- Bundle optimization with code splitting
Production Considerations
// Environment-based API configuration
const getApiConfig = () => {
if (process.env.NODE_ENV === 'development') {
return {
apiUrl: 'https://api-dev.1lookup.io',
timeout: 10000,
retries: 1,
};
}
return {
apiUrl: 'https://api.1lookup.io',
timeout: 5000,
retries: 3,
};
};
// Rate limiting for API calls
const useRateLimitedValidation = (apiKey: string, maxCallsPerMinute: number = 60) => {
const callTimes = useRef<number[]>([]);
const canMakeCall = useCallback(() => {
const now = Date.now();
const oneMinuteAgo = now - 60000;
// Remove calls older than 1 minute
callTimes.current = callTimes.current.filter(time => time > oneMinuteAgo);
return callTimes.current.length < maxCallsPerMinute;
}, [maxCallsPerMinute]);
const validateWithRateLimit = useCallback(async (email: string) => {
if (!canMakeCall()) {
throw new Error('Rate limit exceeded. Please wait before validating more emails.');
}
callTimes.current.push(Date.now());
return validateEmail(email);
}, [canMakeCall, validateEmail]);
return validateWithRateLimit;
};
Get Started with Real-Time Email Validation

Building effective email validation in React requires more than just checking format - it needs real-time API integration that catches the subtle issues that destroy email deliverability and waste marketing budgets.
1Lookup's Email Validation API provides the robust backend your React components need:
Real-Time Validation: Enterprise Accuracy with sub-500ms response times for instant user feedback.
Comprehensive Checks: Format, domain existence, deliverability, disposable detection, and role account identification.
Developer-Friendly: RESTful API with clear responses, detailed documentation, and React-specific examples.
Domain Suggestions: Intelligent typo correction for common email providers (Gmail, Yahoo, Outlook).
Fraud Detection: Risk scoring to identify high-risk emails before they enter your system.
Ready to build bulletproof email validation in your React app? Test our API with 1,000 free validations and see the difference real-time validation makes.
Get your free API key and start building →
Don't let invalid emails drain your marketing budget. Implement real-time email validation today and join the 94% of successful React applications that validate before they send.
Meet the Expert Behind the Insights
Real-world experience from building and scaling B2B SaaS companies

Robby Frank
Head of Growth at 1Lookup
"Calm down, it's just life"
About Robby
Self-taught entrepreneur and technical leader with 12+ years building profitable B2B SaaS companies. Specializes in rapid product development and growth marketing with 1,000+ outreach campaigns executed across industries.
Author of "Evolution of a Maniac" and advocate for practical, results-driven business strategies that prioritize shipping over perfection.