Reputation: 23
I have a form where new users can create a username.
To check if the username is already taken, I use a validation method that verifies its availability. I set the mode of React Hook Form to 'onChange' so that users can see the availability status in real time.
However, I noticed that the form still revalidates on blur. Additionally, on the first submit attempt, it seems to trigger only the onBlur event instead of submitting the form. However, clicking submit again works as expected.
const createDebouncedValidator = () => {
let currentPromise: Promise<boolean> | null = null
const debouncedCheck = debounce((username: string, resolve: (value: boolean) => void) => {
checkAvailability('name', username)
.then(({ available }) => resolve(available))
.catch(() => resolve(false))
}, 500)
return async (username: string) => {
if (username.length < 3) return true
currentPromise = new Promise<boolean>(resolve => {
debouncedCheck(username, resolve)
})
const result = await currentPromise
currentPromise = null
return result
}
}
const onboardingFormSchema = z.object({
username: z
.string()
.min(3, {
message: 'Username must be at least 3 characters.',
})
.max(30, {
message: 'Username must not be longer than 30 characters.',
})
.regex(/^[a-z0-9-]+$/, {
message: 'Username can only contain lowercase letters, numbers, and hyphens.',
})
.refine(name => !name.startsWith('-') && !name.endsWith('-'), {
message: 'Username cannot start or end with a hyphen.',
})
.refine(createDebouncedValidator(), {
message: 'This username is already taken'
})
})
type OnboardingFormValues = z.infer<typeof onboardingFormSchema>
export function OnboardingForm() {
const { t } = useTranslations()
const router = useRouter()
const { setUser } = useAuth()
const { finishOnboarding } = useOnboarding()
const form = useForm<OnboardingFormValues>({
resolver: zodResolver(onboardingFormSchema),
defaultValues: {
username: '',
},
mode: 'onChange',
})
const watchUsername = form.watch('username')
async function onSubmit(data: OnboardingFormValues) {
try {
// Update username
const updatedUser = await updateUsername(data.username)
// Update global user state
setUser(updatedUser)
toast({
title: t('settings.profile.updated'),
description: t('settings.profile.usernameUpdated')
})
// Finish onboarding and redirect
finishOnboarding()
router.push('/links')
} catch (error) {
console.error('Profile update error:', error)
toast({
variant: 'destructive',
title: t('common.error'),
description: t('settings.profile.updateError')
})
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-8'>
<FormField
control={form.control}
name='username'
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<div className="relative">
<Input
{...field}
placeholder='username'
autoComplete='off'
autoCapitalize='off'
/>
{form.formState.isValidating && (
<div className="absolute right-3 top-1/2 -translate-y-1/2">
<IconLoader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
)}
</div>
</FormControl>
<FormDescription className="space-y-2">
<p className="font-mono">
palmlink.eu/
<span className={cn(
watchUsername ? 'text-foreground' : 'text-muted-foreground',
'transition-colors'
)}>
{watchUsername || 'username'}
</span>
</p>
<ul className="text-xs list-disc list-inside space-y-1">
<li>Must be between 3 and 30 characters</li>
<li>Can only contain lowercase letters, numbers, and hyphens</li>
<li>Cannot start or end with a hyphen</li>
</ul>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button
type='submit'
className='w-full'
disabled={!form.formState.isValid || form.formState.isValidating || form.formState.isSubmitting}
>
{form.formState.isSubmitting ? t('common.saving') : 'Continue'}
</Button>
</form>
</Form>
)
}
Upvotes: 1
Views: 21