snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
638
apps/v4/content/docs/forms/react-hook-form.mdx
Normal file
638
apps/v4/content/docs/forms/react-hook-form.mdx
Normal file
@@ -0,0 +1,638 @@
|
||||
---
|
||||
title: React Hook Form
|
||||
description: Build forms in React using React Hook Form and Zod.
|
||||
links:
|
||||
doc: https://react-hook-form.com
|
||||
---
|
||||
|
||||
import { InfoIcon } from "lucide-react"
|
||||
|
||||
In this guide, we will take a look at building forms with React Hook Form. We'll cover building forms with the `<Field />` component, adding schema validation using Zod, error handling, accessibility, and more.
|
||||
|
||||
## Demo
|
||||
|
||||
We are going to build the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.
|
||||
|
||||
<Callout icon={<InfoIcon />}>
|
||||
**Note:** For the purpose of this demo, we have intentionally disabled browser
|
||||
validation to show how schema validation and form errors work in React Hook
|
||||
Form. It is recommended to add basic browser validation in your production
|
||||
code.
|
||||
</Callout>
|
||||
|
||||
<ComponentPreview
|
||||
styleName="new-york-v4"
|
||||
name="form-rhf-demo"
|
||||
className="sm:[&_.preview]:h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
## Approach
|
||||
|
||||
This form leverages React Hook Form for performant, flexible form handling. We'll build our form using the `<Field />` component, which gives you **complete flexibility over the markup and styling**.
|
||||
|
||||
- Uses React Hook Form's `useForm` hook for form state management.
|
||||
- `<Controller />` component for controlled inputs.
|
||||
- `<Field />` components for building accessible forms.
|
||||
- Client-side validation using Zod with `zodResolver`.
|
||||
|
||||
## Anatomy
|
||||
|
||||
Here's a basic example of a form using the `<Controller />` component from React Hook Form and the `<Field />` component.
|
||||
|
||||
```tsx showLineNumbers {5-18}
|
||||
<Controller
|
||||
name="title"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor={field.name}>Bug Title</FieldLabel>
|
||||
<Input
|
||||
{...field}
|
||||
id={field.name}
|
||||
aria-invalid={fieldState.invalid}
|
||||
placeholder="Login button not working on mobile"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>
|
||||
Provide a concise title for your bug report.
|
||||
</FieldDescription>
|
||||
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
## Form
|
||||
|
||||
### Create a form schema
|
||||
|
||||
We'll start by defining the shape of our form using a Zod schema.
|
||||
|
||||
<Callout icon={<InfoIcon />}>
|
||||
**Note:** This example uses `zod v3` for schema validation, but you can
|
||||
replace it with any other Standard Schema validation library supported by
|
||||
React Hook Form.
|
||||
</Callout>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
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."),
|
||||
})
|
||||
```
|
||||
|
||||
### Set up the form
|
||||
|
||||
Next, we'll use the `useForm` hook from React Hook Form to create our form instance. We'll also add the Zod resolver to validate the form data.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {17-23}
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { 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 function BugReportForm() {
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
title: "",
|
||||
description: "",
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof formSchema>) {
|
||||
// Do something with the form values.
|
||||
console.log(data)
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
{/* ... */}
|
||||
{/* Build the form here */}
|
||||
{/* ... */}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Build the form
|
||||
|
||||
We can now build the form using the `<Controller />` component from React Hook Form and the `<Field />` component.
|
||||
|
||||
<ComponentSource
|
||||
src="/registry/new-york-v4/examples/form-rhf-demo.tsx"
|
||||
title="form.tsx"
|
||||
/>
|
||||
|
||||
### Done
|
||||
|
||||
That's it. You now have a fully accessible form with client-side validation.
|
||||
|
||||
When you submit the form, the `onSubmit` function will be called with the validated form data. If the form data is invalid, React Hook Form will display the errors next to each field.
|
||||
|
||||
## Validation
|
||||
|
||||
### Client-side Validation
|
||||
|
||||
React Hook Form validates your form data using the Zod schema. Define a schema and pass it to the `resolver` option of the `useForm` hook.
|
||||
|
||||
```tsx showLineNumbers title="example-form.tsx" {5-8,12}
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
|
||||
const formSchema = z.object({
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
export function ExampleForm() {
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
title: "",
|
||||
description: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Validation Modes
|
||||
|
||||
React Hook Form supports different validation modes.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {3}
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
mode: "onChange",
|
||||
})
|
||||
```
|
||||
|
||||
| Mode | Description |
|
||||
| ------------- | -------------------------------------------------------- |
|
||||
| `"onChange"` | Validation triggers on every change. |
|
||||
| `"onBlur"` | Validation triggers on blur. |
|
||||
| `"onSubmit"` | Validation triggers on submit (default). |
|
||||
| `"onTouched"` | Validation triggers on first blur, then on every change. |
|
||||
| `"all"` | Validation triggers on blur and change. |
|
||||
|
||||
## Displaying Errors
|
||||
|
||||
Display errors next to the field using `<FieldError />`. For styling and accessibility:
|
||||
|
||||
- Add the `data-invalid` prop to the `<Field />` component.
|
||||
- Add the `aria-invalid` prop to the form control such as `<Input />`, `<SelectTrigger />`, `<Checkbox />`, etc.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {5,11,13}
|
||||
<Controller
|
||||
name="email"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
|
||||
<Input
|
||||
{...field}
|
||||
id={field.name}
|
||||
type="email"
|
||||
aria-invalid={fieldState.invalid}
|
||||
/>
|
||||
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
## Working with Different Field Types
|
||||
|
||||
### Input
|
||||
|
||||
- For input fields, spread the `field` object onto the `<Input />` component.
|
||||
- To show errors, add the `aria-invalid` prop to the `<Input />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
styleName="new-york-v4"
|
||||
name="form-rhf-input"
|
||||
className="sm:[&_.preview]:h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
For simple text inputs, spread the `field` object onto the input.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {5,7,8}
|
||||
<Controller
|
||||
name="name"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor={field.name}>Name</FieldLabel>
|
||||
<Input {...field} id={field.name} aria-invalid={fieldState.invalid} />
|
||||
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Textarea
|
||||
|
||||
- For textarea fields, spread the `field` object onto the `<Textarea />` component.
|
||||
- To show errors, add the `aria-invalid` prop to the `<Textarea />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
styleName="new-york-v4"
|
||||
name="form-rhf-textarea"
|
||||
className="sm:[&_.preview]:h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
For textarea fields, spread the `field` object onto the textarea.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {5,10,18}
|
||||
<Controller
|
||||
name="about"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor="form-rhf-textarea-about">More about you</FieldLabel>
|
||||
<Textarea
|
||||
{...field}
|
||||
id="form-rhf-textarea-about"
|
||||
aria-invalid={fieldState.invalid}
|
||||
placeholder="I'm a software engineer..."
|
||||
className="min-h-[120px]"
|
||||
/>
|
||||
<FieldDescription>
|
||||
Tell us more about yourself. This will be used to help us personalize
|
||||
your experience.
|
||||
</FieldDescription>
|
||||
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Select
|
||||
|
||||
- For select components, use `field.value` and `field.onChange` on the `<Select />` component.
|
||||
- To show errors, add the `aria-invalid` prop to the `<SelectTrigger />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
styleName="new-york-v4"
|
||||
name="form-rhf-select"
|
||||
className="sm:[&_.preview]:h-[500px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {5,13,22}
|
||||
<Controller
|
||||
name="language"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field orientation="responsive" data-invalid={fieldState.invalid}>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="form-rhf-select-language">
|
||||
Spoken Language
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
For best results, select the language you speak.
|
||||
</FieldDescription>
|
||||
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||
</FieldContent>
|
||||
<Select
|
||||
name={field.name}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="form-rhf-select-language"
|
||||
aria-invalid={fieldState.invalid}
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="item-aligned">
|
||||
<SelectItem value="auto">Auto</SelectItem>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Checkbox
|
||||
|
||||
- For checkbox arrays, use `field.value` and `field.onChange` with array manipulation.
|
||||
- To show errors, add the `aria-invalid` prop to the `<Checkbox />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
- Remember to add `data-slot="checkbox-group"` to the `<FieldGroup />` component for proper styling and spacing.
|
||||
|
||||
<ComponentPreview
|
||||
styleName="new-york-v4"
|
||||
name="form-rhf-checkbox"
|
||||
className="sm:[&_.preview]:h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {10,15,20-22,38}
|
||||
<Controller
|
||||
name="tasks"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label">Tasks</FieldLegend>
|
||||
<FieldDescription>
|
||||
Get notified when tasks you've created have updates.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
{tasks.map((task) => (
|
||||
<Field
|
||||
key={task.id}
|
||||
orientation="horizontal"
|
||||
data-invalid={fieldState.invalid}
|
||||
>
|
||||
<Checkbox
|
||||
id={`form-rhf-checkbox-${task.id}`}
|
||||
name={field.name}
|
||||
aria-invalid={fieldState.invalid}
|
||||
checked={field.value.includes(task.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
const newValue = checked
|
||||
? [...field.value, task.id]
|
||||
: field.value.filter((value) => value !== task.id)
|
||||
field.onChange(newValue)
|
||||
}}
|
||||
/>
|
||||
<FieldLabel
|
||||
htmlFor={`form-rhf-checkbox-${task.id}`}
|
||||
className="font-normal"
|
||||
>
|
||||
{task.label}
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
))}
|
||||
</FieldGroup>
|
||||
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||
</FieldSet>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Radio Group
|
||||
|
||||
- For radio groups, use `field.value` and `field.onChange` on the `<RadioGroup />` component.
|
||||
- To show errors, add the `aria-invalid` prop to the `<RadioGroupItem />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
styleName="new-york-v4"
|
||||
name="form-rhf-radiogroup"
|
||||
className="sm:[&_.preview]:h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {12-13,17,25,31}
|
||||
<Controller
|
||||
name="plan"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<FieldSet>
|
||||
<FieldLegend>Plan</FieldLegend>
|
||||
<FieldDescription>
|
||||
You can upgrade or downgrade your plan at any time.
|
||||
</FieldDescription>
|
||||
<RadioGroup
|
||||
name={field.name}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
{plans.map((plan) => (
|
||||
<FieldLabel key={plan.id} htmlFor={`form-rhf-radiogroup-${plan.id}`}>
|
||||
<Field orientation="horizontal" data-invalid={fieldState.invalid}>
|
||||
<FieldContent>
|
||||
<FieldTitle>{plan.title}</FieldTitle>
|
||||
<FieldDescription>{plan.description}</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value={plan.id}
|
||||
id={`form-rhf-radiogroup-${plan.id}`}
|
||||
aria-invalid={fieldState.invalid}
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||
</FieldSet>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Switch
|
||||
|
||||
- For switches, use `field.value` and `field.onChange` on the `<Switch />` component.
|
||||
- To show errors, add the `aria-invalid` prop to the `<Switch />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
styleName="new-york-v4"
|
||||
name="form-rhf-switch"
|
||||
className="sm:[&_.preview]:h-[500px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {5,13,18-19}
|
||||
<Controller
|
||||
name="twoFactor"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field orientation="horizontal" data-invalid={fieldState.invalid}>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="form-rhf-switch-twoFactor">
|
||||
Multi-factor authentication
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Enable multi-factor authentication to secure your account.
|
||||
</FieldDescription>
|
||||
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||
</FieldContent>
|
||||
<Switch
|
||||
id="form-rhf-switch-twoFactor"
|
||||
name={field.name}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
aria-invalid={fieldState.invalid}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Complex Forms
|
||||
|
||||
Here is an example of a more complex form with multiple fields and validation.
|
||||
|
||||
<ComponentPreview
|
||||
styleName="new-york-v4"
|
||||
name="form-rhf-complex"
|
||||
className="sm:[&_.preview]:h-[1300px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
## Resetting the Form
|
||||
|
||||
Use `form.reset()` to reset the form to its default values.
|
||||
|
||||
```tsx showLineNumbers
|
||||
<Button type="button" variant="outline" onClick={() => form.reset()}>
|
||||
Reset
|
||||
</Button>
|
||||
```
|
||||
|
||||
## Array Fields
|
||||
|
||||
React Hook Form provides a `useFieldArray` hook for managing dynamic array fields. This is useful when you need to add or remove fields dynamically.
|
||||
|
||||
<ComponentPreview
|
||||
styleName="new-york-v4"
|
||||
name="form-rhf-array"
|
||||
className="sm:[&_.preview]:h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
### Using useFieldArray
|
||||
|
||||
Use the `useFieldArray` hook to manage array fields. It provides `fields`, `append`, and `remove` methods.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {8-11}
|
||||
import { useFieldArray, useForm } from "react-hook-form"
|
||||
|
||||
export function ExampleForm() {
|
||||
const form = useForm({
|
||||
// ... form config
|
||||
})
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "emails",
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Array Field Structure
|
||||
|
||||
Wrap your array fields in a `<FieldSet />` with a `<FieldLegend />` and `<FieldDescription />`.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
<FieldSet className="gap-4">
|
||||
<FieldLegend variant="label">Email Addresses</FieldLegend>
|
||||
<FieldDescription>
|
||||
Add up to 5 email addresses where we can contact you.
|
||||
</FieldDescription>
|
||||
<FieldGroup className="gap-4">{/* Array items go here */}</FieldGroup>
|
||||
</FieldSet>
|
||||
```
|
||||
|
||||
### Controller Pattern for Array Items
|
||||
|
||||
Map over the `fields` array and use `<Controller />` for each item. **Make sure to use `field.id` as the key**.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
{
|
||||
fields.map((field, index) => (
|
||||
<Controller
|
||||
key={field.id}
|
||||
name={`emails.${index}.address`}
|
||||
control={form.control}
|
||||
render={({ field: controllerField, fieldState }) => (
|
||||
<Field orientation="horizontal" data-invalid={fieldState.invalid}>
|
||||
<FieldContent>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
{...controllerField}
|
||||
id={`form-rhf-array-email-${index}`}
|
||||
aria-invalid={fieldState.invalid}
|
||||
placeholder="name@example.com"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
/>
|
||||
{/* Remove button */}
|
||||
</InputGroup>
|
||||
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||
</FieldContent>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Items
|
||||
|
||||
Use the `append` method to add new items to the array.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => append({ address: "" })}
|
||||
disabled={fields.length >= 5}
|
||||
>
|
||||
Add Email Address
|
||||
</Button>
|
||||
```
|
||||
|
||||
### Removing Items
|
||||
|
||||
Use the `remove` method to remove items from the array. Add the remove button conditionally.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
{
|
||||
fields.length > 1 && (
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => remove(index)}
|
||||
aria-label={`Remove email ${index + 1}`}
|
||||
>
|
||||
<XIcon />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Array Validation
|
||||
|
||||
Use Zod's `array` method to validate array fields.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
const formSchema = z.object({
|
||||
emails: z
|
||||
.array(
|
||||
z.object({
|
||||
address: z.string().email("Enter a valid email address."),
|
||||
})
|
||||
)
|
||||
.min(1, "Add at least one email address.")
|
||||
.max(5, "You can add up to 5 email addresses."),
|
||||
})
|
||||
```
|
||||
Reference in New Issue
Block a user