Simplifying Form Handling with React Hook Form
Learn how to create powerful forms in React using React Hook Form. This comprehensive guide covers everything from basic setup to advanced features.
- React
- Next.js
- React Hook Form
- Zod
Introduction
Hello everynyan! Today, we're going to learn how to use React Hook Form with Next.js, a powerful combination that makes form handling a breeze.
Setup
- First, let's create a new Next.js app. You can use my pre-configured template by running the following command:
npx create-t3-app
- Next, install
react-hook-form
:
bun add react-hook-form zod @hookform/resolvers
- Thirdly, let's add some components form
shadcn/ui
:
npx shadcn@latest add input textarea label button sonner
npx shadcn@latest add input textarea label button sonner
pnpm dlx shadcn@latest add input textarea label button sonner
bunx --bun shadcn@latest add input textarea label button sonner
Remember to add
Toaster
component to yourlayout.tsx
file.
- Finally, let's create a new form component in
components/ui/form.tsx
:
import { forwardRef } from 'react'
import { cn } from '@/lib/utils'
type FormProps = React.FormHTMLAttributes<HTMLFormElement>
export const Form = forwardRef<HTMLFormElement, FormProps>(
({ className = '', ...props }, ref) => (
<form
{...props}
ref={ref}
className={cn('flex flex-col gap-4', className)}
/>
),
)
Form.displayName = 'Form'
import type { FieldValues, Path, UseFormReturn } from 'react-hook-form'
import { useId } from 'react'
import { Slot } from '@radix-ui/react-slot'
import { Controller } from 'react-hook-form'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { cn } from '@/lib/utils'
interface FormFieldProps<T extends FieldValues = FieldValues>
extends Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'name' | 'onChange' | 'onBlur'
> {
name: Path<T>
control: UseFormReturn<T>['control']
label?: string
description?: string
asChild?: boolean
classes?: {
root?: string
label?: string
input?: string
description?: string
message?: string
}
}
export const FormField = <T extends FieldValues>({
name,
control,
label = '',
description = '',
asChild = false,
classes = {},
...props
}: FormFieldProps<T>): React.ReactElement => {
const id = useId()
const ids = {
field: `${id}-form-field`,
description: `${id}-form-field-description`,
message: `${id}-form-field-message`,
}
const Comp = asChild ? Slot : Input
return (
<Controller
name={name}
control={control}
render={({
field,
fieldState: { error },
formState: { isSubmitting },
}) => (
<fieldset
name={field.name}
disabled={isSubmitting}
className={cn('space-y-2', classes.root)}
>
{label && (
<Label
htmlFor={ids.field}
className={cn(error && 'text-destructive', classes.label)}
>
{label}
</Label>
)}
<Comp
{...field}
{...props}
id={ids.field}
className={cn(
error && 'border-destructive focus-visible:outline-destructive',
classes.input,
)}
aria-describedby={
error ? `${ids.description} ${ids.message}` : ids.description
}
aria-invalid={!!error}
/>
{description && (
<p
id={ids.description}
className={cn(
'text-muted-foreground text-sm',
classes.description,
)}
>
{description}
</p>
)}
{error && (
<small
id={ids.message}
className={cn('text-destructive', classes.message)}
>
{error.message}
</small>
)}
</fieldset>
)}
/>
)
}
Usage
- Create a form schema:
import { z } from 'zod'
const schema = z
.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email(),
password: z
.string()
.regex(
/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*\W).{8,}$/,
'Password must contain at least 8 characters, including uppercase, lowercase, number, and special character.',
),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
path: ['confirmPassword'],
message: 'Passwords do not match',
})
type FormValues = z.infer<typeof schema>
- Define the form's fields:
const fields = [
{ name: 'name' as const, label: 'Name', type: 'text' },
{ name: 'email' as const, label: 'Email', type: 'email' },
{ name: 'password' as const, label: 'Password', type: 'password' },
{
name: 'confirmPassword' as const,
label: 'Confirm Password',
type: 'password',
},
]
- Create the form component:
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import { Form, FormField } from '@/components/ui/form'
export const SignupForm: React.FC = () => {
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
})
const handleSubmit = form.handleSubmit(async (data) => {
/* Call your API here */
await new Promise((resolve) => setTimeout(resolve, 1000))
toast.success('Account created!', {
description: <pre>{JSON.stringify(data, null, 2)}</pre>,
})
})
return (
<Form className="w-full max-w-(--breakpoint-md)" onSubmit={handleSubmit}>
{fields.map((field) => (
<FormField key={field.name} control={form.control} {...field} />
))}
<Button isLoading={form.formState.isSubmitting}>Register</Button>
</Form>
)
}
Conclusion
And that's it! We've successfully created a form using React Hook Form with Next.js. This powerful combination makes form handling a breeze, and with the help of zod
for schema validation, we can ensure that our data is always correct. I hope you found this tutorial helpful, and I'll see you in the next one!
Repository: tiesen243/rhf
References
- Next.js: nextjs.org
- React Hook Form: react-hook-form.com
- Zod: zod.dev
Build Your Own Image Classifier with PyTorch
Dive into the world of Convolutional Neural Networks (CNNs) by building your own image classifier using PyTorch. This blog explores the steps involved, referencing the tiesen243/cnn repository on GitHub.
Setting Up a Monorepo with Turborepo
Explore the key concepts of monorepo architecture and how Turborepo can enhance your development productivity. This in-depth guide will cover topics such as caching, workspaces, and dependency management.