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.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
Share this code
Here's the card — post it anywhere.