import { useForm } from 'react-hook-form'
import { useState } from 'react'
import api from '@/services/api'
interface SignupFormData {
email: string
username: string
password: string
}
export function SignupForm() {
const [validatingUsername, setValidatingUsername] = useState(false)
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<SignupFormData>()
const checkUsernameAvailability = async (username: string) => {
if (username.length < 3) {
return 'Username must be at least 3 characters'
}
setValidatingUsername(true)
try {
const { data } = await api.get(`/auth/check-username?username=${username}`)
if (!data.available) {
return 'Username is already taken'
}
return true
} catch (error) {
return 'Unable to verify username'
} finally {
setValidatingUsername(false)
}
}
const onSubmit = async (data: SignupFormData) => {
try {
await api.post('/auth/signup', data)
window.location.href = '/dashboard'
} catch (error: any) {
console.error('Signup failed:', error)
}
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<label htmlFor="email">Email</label>
<input
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address',
},
})}
type="email"
id="email"
className="w-full px-3 py-2 border rounded"
/>
{errors.email && <p className="text-red-600 text-sm mt-1">{errors.email.message}</p>}
</div>
<div>
<label htmlFor="username">Username</label>
<div className="relative">
<input
{...register('username', {
required: 'Username is required',
validate: checkUsernameAvailability,
})}
id="username"
className="w-full px-3 py-2 border rounded"
/>
{validatingUsername && (
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600" />
</div>
)}
</div>
{errors.username && <p className="text-red-600 text-sm mt-1">{errors.username.message}</p>}
</div>
<div>
<label htmlFor="password">Password</label>
<input
{...register('password', {
required: 'Password is required',
minLength: { value: 8, message: 'Password must be at least 8 characters' },
})}
type="password"
id="password"
className="w-full px-3 py-2 border rounded"
/>
{errors.password && <p className="text-red-600 text-sm mt-1">{errors.password.message}</p>}
</div>
<button
type="submit"
disabled={isSubmitting || validatingUsername}
className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{isSubmitting ? 'Creating account...' : 'Sign Up'}
</button>
</form>
)
}
Async validation checks constraints that require server communication, like username availability or email uniqueness. React Hook Form's validate option accepts async functions that return error messages or true. I debounce async validators to avoid excessive requests and show loading states during validation. The form submission waits for all async validations to complete. For better UX, I show validation results inline as users type, with clear messaging about what's being checked. Async validation combines with Zod schemas using custom refinements. The key is balancing responsiveness (early feedback) with efficiency (not spamming the server). This pattern provides real-time validation without sacrificing performance.