React Hook Form Example
Build forms in React using React Hook Form and Zod.
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { Button } from '@nerdfish/react/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@nerdfish/react/card'
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from '@nerdfish/react/field'
import { Input } from '@nerdfish/react/input'
import {
InputGroup,
InputGroupAddon,
InputGroupText,
InputGroupTextarea,
} from '@nerdfish/react/input-group'
import { toast } from '@nerdfish/react/toast'
import { type CSSProperties } from 'react'
import { Controller, useForm } from 'react-hook-form'
import * as z from 'zod'
const formSchema = z.object({
title: z
.string()
.min(5, 'Bug title must be at least 5 characters.')
.max(32, 'Bug title must be at most 32 characters.'),
description: z
.string()
.min(20, 'Description must be at least 20 characters.')
.max(100, 'Description must be at most 100 characters.'),
})
export default function ReactHookFormExample() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
title: '',
description: '',
},
})
function onSubmit(data: z.infer<typeof formSchema>) {
toast('You submitted the following values:', {
description: (
<pre className="bg-muted/50 text-foreground p-friends mt-best-friends rounded-compact w-80 overflow-x-auto">
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
),
position: 'bottom-right',
classNames: {
content: 'flex flex-col gap-best-friends',
},
style: {
'--border-radius': 'calc(var(--radius) + 4px)',
} as CSSProperties,
})
}
return (
<Card className="w-full max-w-2xl">
<CardHeader>
<CardTitle>Bug Report</CardTitle>
<CardDescription>
Help us improve by reporting bugs you encounter.
</CardDescription>
</CardHeader>
<CardContent>
<form id="form-rhf-demo" onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
name="title"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="form-rhf-demo-title">
Bug Title
</FieldLabel>
<Input
{...field}
id="form-rhf-demo-title"
aria-invalid={fieldState.invalid}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
{fieldState.invalid ? (
<FieldError errors={[fieldState.error]} />
) : null}
</Field>
)}
/>
<Controller
name="description"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="form-rhf-demo-description">
Description
</FieldLabel>
<InputGroup>
<InputGroupTextarea
{...field}
id="form-rhf-demo-description"
placeholder="I'm having an issue with the login button on mobile."
rows={6}
className="min-h-24 resize-none"
aria-invalid={fieldState.invalid}
/>
<InputGroupAddon align="block-end">
<InputGroupText className="tabular-nums">
{field.value.length}/100 characters
</InputGroupText>
</InputGroupAddon>
</InputGroup>
<FieldDescription>
Include steps to reproduce, expected behavior, and what
actually happened.
</FieldDescription>
{fieldState.invalid ? (
<FieldError errors={[fieldState.error]} />
) : null}
</Field>
)}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" form="form-rhf-demo">
Submit
</Button>
</Field>
</CardFooter>
</Card>
)
}