Form Patterns
Common form patterns using React Hook Form, Zod validation, and Clerk authentication.
Form patterns in Plainform use React Hook Form for state management, Zod for validation, and integrate seamlessly with Clerk for authentication flows.
Form Stack
- React Hook Form: Form state and validation
- Zod: Schema validation with TypeScript inference
- @hookform/resolvers: Connects Zod schemas to React Hook Form
- Clerk: Authentication API integration
- Sonner: Toast notifications for errors
Basic Form Pattern
Setup
'use client';
import { useForm, SubmitHandler } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Input } from '@/components/ui/Input';
import { Label } from '@/components/ui/Label';
import { Button } from '@/components/ui/Button';
// Define schema
const formSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
type FormData = z.infer<typeof formSchema>;
export function MyForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>({
mode: 'onTouched',
resolver: zodResolver(formSchema),
});
const onSubmit: SubmitHandler<FormData> = async (data) => {
// Handle form submission
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Label htmlFor="email">Email</Label>
<Input
{...register('email')}
id="email"
type="email"
placeholder="you@example.com"
errorMessage={errors.email?.message}
isInvalid={!!errors.email}
disabled={isSubmitting}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="password">Password</Label>
<Input
{...register('password')}
id="password"
type="password"
placeholder="●●●●●●●●"
errorMessage={errors.password?.message}
isInvalid={!!errors.password}
disabled={isSubmitting}
/>
</div>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</Button>
</form>
);
}Clerk Authentication Pattern
Sign In Form
'use client';
import { useSignIn } from '@clerk/nextjs';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';
import { ClerkAPIError } from '@clerk/types';
export function SignInForm() {
const { signIn, setActive, isLoaded } = useSignIn();
const router = useRouter();
const {
register,
handleSubmit,
setError,
formState: { errors, isSubmitting },
} = useForm<FormData>({
resolver: zodResolver(signInSchema),
});
const onSubmit: SubmitHandler<FormData> = async (data) => {
if (!isLoaded) return;
try {
const result = await signIn.create({
identifier: data.email,
password: data.password,
});
if (result.status === 'complete') {
await setActive({ session: result.createdSessionId });
router.push('/');
}
} catch (err: any) {
err.errors.forEach((error: ClerkAPIError) => {
const paramName = error.meta?.paramName as keyof FormData;
if (paramName && error.longMessage) {
setError(paramName, {
type: 'manual',
message: error.longMessage,
});
} else {
toast.error(error.message);
}
});
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* Form fields */}
</form>
);
}Validation Schemas
Store schemas in validationSchemas/ directory:
import { z } from 'zod';
export const signInSchema = z.object({
identifier: z.string().email('Please enter a valid email address'),
password: z.string().min(1, 'Password is required'),
});
export const signUpSchema = z.object({
emailAddress: z.string().email('Please enter a valid email address'),
password: z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain an uppercase letter')
.regex(/[0-9]/, 'Password must contain a number'),
firstName: z.string().min(1, 'First name is required'),
lastName: z.string().min(1, 'Last name is required'),
});Error Handling Patterns
Field-Level Errors
<Input
{...register('email')}
errorMessage={errors.email?.message}
isInvalid={!!errors.email}
/>API Errors with Toast
catch (err: any) {
if (err.errors) {
err.errors.forEach((error: ClerkAPIError) => {
const field = error.meta?.paramName as keyof FormData;
if (field) {
setError(field, { message: error.longMessage });
} else {
toast.error(error.message);
}
});
}
}Form Components
StepHeader
interface IStepHeader {
title: string;
description: string;
}
export function StepHeader({ title, description }: IStepHeader) {
return (
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-bold">{title}</h1>
<p className="text-neutral-foreground">{description}</p>
</div>
);
}StepFooter
interface IStepFooter {
title: string;
buttonText: string;
href: string;
}
export function StepFooter({ title, buttonText, href }: IStepFooter) {
return (
<p className="text-sm text-neutral-foreground">
{title}{' '}
<Link href={href} className="text-foreground font-medium">
{buttonText}
</Link>
</p>
);
}Loading States
import { BeatLoader } from 'react-spinners';
<Button disabled={isSubmitting} type="submit">
{isSubmitting && (
<BeatLoader size={5} className="[&>span]:!bg-foreground" />
)}
{isSubmitting ? 'Signing in...' : 'Sign In'}
</Button>Related
- React Hook Form - Form library documentation
- Zod - Schema validation
- Clerk Authentication - Auth integration
- Input.tsx - Input component
How is this guide ?
Last updated on