🌐 Nodejs.cn

TanStack 表单

使用 TanStack Form 和 Zod 在 React 中构建表单。

本指南探讨了如何使用 TanStack Form 构建表单。你将学习使用 <Field /> 组件创建表单,使用 Zod 实现模式验证,处理错误,并确保可访问性。

🌐 This guide explores how to build forms using TanStack Form. You'll learn to create forms with the <Field /> component, implement schema validation with Zod, handle errors, and ensure accessibility.

演示

🌐 Demo

我们将从构建以下表单开始。它有一个简单的文本输入框和一个多行文本框。提交时,我们将验证表单数据并显示任何错误。

🌐 We'll start by building the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.

Bug Report
Help us improve by reporting bugs you encounter.
0/100 characters

Include steps to reproduce, expected behavior, and what actually happened.

"use client"

import * as React from "react"

方法

🌐 Approach

此表单利用 TanStack Form 提供强大的无头表单处理功能。我们将使用 <Field /> 组件来构建表单,该组件为你提供 对标记和样式的完全灵活性

🌐 This form leverages TanStack Form for powerful, headless form handling. We'll build our form using the <Field /> component, which gives you complete flexibility over the markup and styling.

  • 使用 TanStack Form 的 useForm 钩子进行表单状态管理。
  • form.Field 组件,使用渲染属性模式来控制输入。
  • <Field /> 组件用于构建可访问的表单。
  • 使用 Zod 进行客户端验证。
  • 实时验证反馈。

剖析

🌐 Anatomy

这是一个使用 TanStack Form 和 <Field /> 组件的表单的基本示例。

🌐 Here's a basic example of a form using TanStack Form with the <Field /> component.

<form
  onSubmit={(e) => {
    e.preventDefault()
    form.handleSubmit()
  }}
>
  <FieldGroup>
    <form.Field
      name="title"
      children={(field) => {
        const isInvalid =
          field.state.meta.isTouched && !field.state.meta.isValid
        return (
          <Field data-invalid={isInvalid}>
            <FieldLabel htmlFor={field.name}>Bug Title</FieldLabel>
            <Input
              id={field.name}
              name={field.name}
              value={field.state.value}
              onBlur={field.handleBlur}
              onChange={(e) => field.handleChange(e.target.value)}
              aria-invalid={isInvalid}
              placeholder="Login button not working on mobile"
              autoComplete="off"
            />
            <FieldDescription>
              Provide a concise title for your bug report.
            </FieldDescription>
            {isInvalid && <FieldError errors={field.state.meta.errors} />}
          </Field>
        )
      }}
    />
  </FieldGroup>
  <Button type="submit">Submit</Button>
</form>

表单

🌐 Form

创建一个模式

🌐 Create a schema

我们将首先使用 Zod 架构来定义表格的形状。

🌐 We'll start by defining the shape of our form using a Zod schema.

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."),
})

设置表单

🌐 Setup the form

使用 TanStack Form 的 useForm 钩子创建带有 Zod 验证的表单实例。

🌐 Use the useForm hook from TanStack Form to create your form instance with Zod validation.

form.tsx
import { useForm } from "@tanstack/react-form"
import { toast } from "sonner"
import * as z from "zod"
 
const formSchema = z.object({
  // ...
})
 
export function BugReportForm() {
  const form = useForm({
    defaultValues: {
      title: "",
      description: "",
    },
    validators: {
      onSubmit: formSchema,
    },
    onSubmit: async ({ value }) => {
      toast.success("Form submitted successfully")
    },
  })
 
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        form.handleSubmit()
      }}
    >
      
    </form>
  )
}

我们在这里使用 onSubmit 来验证表单数据。TanStack Form 支持其他验证模式,你可以在文档中阅读相关内容。

🌐 We are using onSubmit to validate the form data here. TanStack Form supports other validation modes, which you can read about in the documentation.

建立表单

🌐 Build the form

我们现在可以使用 TanStack Form 的 form.Field 组件和 <Field /> 组件来构建表单。

🌐 We can now build the form using the form.Field component from TanStack Form and the <Field /> component.

form.tsx
"use client"

import * as React from "react"
import { useForm } from "@tanstack/react-form"
import { toast } from "sonner"
import * as z from "zod"

import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import {
  Field,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import {
  InputGroup,
  InputGroupAddon,
  InputGroupText,
  InputGroupTextarea,
} from "@/components/ui/input-group"

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({
    defaultValues: {
      title: "",
      description: "",
    },
    validators: {
      onSubmit: formSchema,
    },
    onSubmit: async ({ value }) => {
      toast("You submitted the following values:", {
        description: (
          <pre className="mt-2 w-[320px] overflow-x-auto rounded-md bg-code p-4 text-code-foreground">
            <code>{JSON.stringify(value, null, 2)}</code>
          </pre>
        ),
        position: "bottom-right",
        classNames: {
          content: "flex flex-col gap-2",
        },
        style: {
          "--border-radius": "calc(var(--radius)  + 4px)",
        } as React.CSSProperties,
      })
    },
  })

  return (
    <Card className="w-full sm:max-w-md">
      <CardHeader>
        <CardTitle>Bug Report</CardTitle>
        <CardDescription>
          Help us improve by reporting bugs you encounter.
        </CardDescription>
      </CardHeader>
      <CardContent>
        <form
          id="bug-report-form"
          onSubmit={(e) => {
            e.preventDefault()
            form.handleSubmit()
          }}
        >
          <FieldGroup>
            <form.Field
              name="title"
              children={(field) => {
                const isInvalid =
                  field.state.meta.isTouched && !field.state.meta.isValid
                return (
                  <Field data-invalid={isInvalid}>
                    <FieldLabel htmlFor={field.name}>Bug Title</FieldLabel>
                    <Input
                      id={field.name}
                      name={field.name}
                      value={field.state.value}
                      onBlur={field.handleBlur}
                      onChange={(e) => field.handleChange(e.target.value)}
                      aria-invalid={isInvalid}
                      placeholder="Login button not working on mobile"
                      autoComplete="off"
                    />
                    {isInvalid && (
                      <FieldError errors={field.state.meta.errors} />
                    )}
                  </Field>
                )
              }}
            />
            <form.Field
              name="description"
              children={(field) => {
                const isInvalid =
                  field.state.meta.isTouched && !field.state.meta.isValid
                return (
                  <Field data-invalid={isInvalid}>
                    <FieldLabel htmlFor={field.name}>Description</FieldLabel>
                    <InputGroup>
                      <InputGroupTextarea
                        id={field.name}
                        name={field.name}
                        value={field.state.value}
                        onBlur={field.handleBlur}
                        onChange={(e) => field.handleChange(e.target.value)}
                        placeholder="I'm having an issue with the login button on mobile."
                        rows={6}
                        className="min-h-24 resize-none"
                        aria-invalid={isInvalid}
                      />
                      <InputGroupAddon align="block-end">
                        <InputGroupText className="tabular-nums">
                          {field.state.value.length}/100 characters
                        </InputGroupText>
                      </InputGroupAddon>
                    </InputGroup>
                    <FieldDescription>
                      Include steps to reproduce, expected behavior, and what
                      actually happened.
                    </FieldDescription>
                    {isInvalid && (
                      <FieldError errors={field.state.meta.errors} />
                    )}
                  </Field>
                )
              }}
            />
          </FieldGroup>
        </form>
      </CardContent>
      <CardFooter>
        <Field orientation="horizontal">
          <Button type="button" variant="outline" onClick={() => form.reset()}>
            Reset
          </Button>
          <Button type="submit" form="bug-report-form">
            Submit
          </Button>
        </Field>
      </CardFooter>
    </Card>
  )
}

完成

🌐 Done

就是这样。你现在有一个完全可访问的表格,并带有客户端验证。

🌐 That's it. You now have a fully accessible form with client-side validation.

当你提交表单时,onSubmit 函数将使用验证过的表单数据被调用。如果表单数据无效,TanStack Form 会在每个字段旁显示错误信息。

🌐 When you submit the form, the onSubmit function will be called with the validated form data. If the form data is invalid, TanStack Form will display the errors next to each field.

验证

🌐 Validation

客户端验证

🌐 Client-side Validation

TanStack Form 使用 Zod 模式验证你的表单数据。验证会在用户输入时实时发生。

🌐 TanStack Form validates your form data using the Zod schema. Validation happens in real-time as the user types.

form.tsx
import { useForm } from "@tanstack/react-form"
 
const formSchema = z.object({
  // ...
})
 
export function BugReportForm() {
  const form = useForm({
    defaultValues: {
      title: "",
      description: "",
    },
    validators: {
      onSubmit: formSchema,
    },
    onSubmit: async ({ value }) => {
      console.log(value)
    },
  })
 
  return <form onSubmit=></form>
}

验证模式

🌐 Validation Modes

TanStack 表单通过 validators 选项支持不同的验证策略:

🌐 TanStack Form supports different validation strategies through the validators option:

模式描述
"onChange"验证在每次更改时触发。
"onBlur"验证在失去焦点时触发。
"onSubmit"验证在提交时触发。
form.tsx
const form = useForm({
  defaultValues: {
    title: "",
    description: "",
  },
  validators: {
    onSubmit: formSchema,
    onChange: formSchema,
    onBlur: formSchema,
  },
})

显示错误

🌐 Displaying Errors

使用 <FieldError /> 在字段旁显示错误。用于样式和可访问性:

🌐 Display errors next to the field using <FieldError />. For styling and accessibility:

  • data-invalid 属性添加到 <Field /> 组件中。
  • aria-invalid 属性添加到表单控件中,例如 <Input /><SelectTrigger /><Checkbox /> 等。
form.tsx
<form.Field
  name="email"
  children={(field) => {
    const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
 
    return (
      <Field data-invalid={isInvalid}>
        <FieldLabel htmlFor={field.name}>Email</FieldLabel>
        <Input
          id={field.name}
          name={field.name}
          value={field.state.value}
          onBlur={field.handleBlur}
          onChange={(e) => field.handleChange(e.target.value)}
          type="email"
          aria-invalid={isInvalid}
        />
        {isInvalid && <FieldError errors={field.state.meta.errors} />}
      </Field>
    )
  }}
/>

处理不同的字段类型

🌐 Working with Different Field Types

输入

🌐 Input

  • 对于输入字段,在 <Input /> 组件上使用 field.state.valuefield.handleChange
  • 要显示错误,请在 <Input /> 组件中添加 aria-invalid 属性,在 <Field /> 组件中添加 data-invalid 属性。
Profile Settings
Update your profile information below.

This is your public display name. Must be between 3 and 10 characters. Must only contain letters, numbers, and underscores.

"use client"

import { useForm } from "@tanstack/react-form"
form.tsx
<form.Field
  name="username"
  children={(field) => {
    const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
    return (
      <Field data-invalid={isInvalid}>
        <FieldLabel htmlFor="form-tanstack-input-username">Username</FieldLabel>
        <Input
          id="form-tanstack-input-username"
          name={field.name}
          value={field.state.value}
          onBlur={field.handleBlur}
          onChange={(e) => field.handleChange(e.target.value)}
          aria-invalid={isInvalid}
          placeholder="shadcn"
          autoComplete="username"
        />
        <FieldDescription>
          This is your public display name. Must be between 3 and 10 characters.
          Must only contain letters, numbers, and underscores.
        </FieldDescription>
        {isInvalid && <FieldError errors={field.state.meta.errors} />}
      </Field>
    )
  }}
/>

文本区域

🌐 Textarea

  • 对于文本区域字段,在 <Textarea /> 组件上使用 field.state.valuefield.handleChange
  • 要显示错误,请在 <Textarea /> 组件中添加 aria-invalid 属性,在 <Field /> 组件中添加 data-invalid 属性。
Personalization
Customize your experience by telling us more about yourself.

Tell us more about yourself. This will be used to help us personalize your experience.

"use client"

import { useForm } from "@tanstack/react-form"
form.tsx
<form.Field
  name="about"
  children={(field) => {
    const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
    return (
      <Field data-invalid={isInvalid}>
        <FieldLabel htmlFor="form-tanstack-textarea-about">
          More about you
        </FieldLabel>
        <Textarea
          id="form-tanstack-textarea-about"
          name={field.name}
          value={field.state.value}
          onBlur={field.handleBlur}
          onChange={(e) => field.handleChange(e.target.value)}
          aria-invalid={isInvalid}
          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>
        {isInvalid && <FieldError errors={field.state.meta.errors} />}
      </Field>
    )
  }}
/>

选择

🌐 Select

  • 对于某些组件,在 <Select /> 组件上使用 field.state.valuefield.handleChange
  • 要显示错误,请在 <SelectTrigger /> 组件中添加 aria-invalid 属性,在 <Field /> 组件中添加 data-invalid 属性。
Language Preferences
Select your preferred spoken language.

For best results, select the language you speak.

"use client"

import { useForm } from "@tanstack/react-form"
form.tsx
<form.Field
  name="language"
  children={(field) => {
    const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
    return (
      <Field orientation="responsive" data-invalid={isInvalid}>
        <FieldContent>
          <FieldLabel htmlFor="form-tanstack-select-language">
            Spoken Language
          </FieldLabel>
          <FieldDescription>
            For best results, select the language you speak.
          </FieldDescription>
          {isInvalid && <FieldError errors={field.state.meta.errors} />}
        </FieldContent>
        <Select
          name={field.name}
          value={field.state.value}
          onValueChange={field.handleChange}
        >
          <SelectTrigger
            id="form-tanstack-select-language"
            aria-invalid={isInvalid}
            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

  • 对于复选框,在 <Checkbox /> 组件上使用 field.state.valuefield.handleChange
  • 要显示错误,请在 <Checkbox /> 组件中添加 aria-invalid 属性,在 <Field /> 组件中添加 data-invalid 属性。
  • 对于复选框数组,在 <form.Field /> 组件上使用 mode="array" 和 TanStack Form 的数组辅助函数。
  • 记得将 data-slot="checkbox-group" 添加到 <FieldGroup /> 组件中,以获得正确的样式和间距。
Notifications
Manage your notification preferences.
Responses

Get notified for requests that take time, like research or image generation.

Tasks

Get notified when tasks you've created have updates.

"use client"

import { useForm } from "@tanstack/react-form"
form.tsx
<form.Field
  name="tasks"
  mode="array"
  children={(field) => {
    const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
    return (
      <FieldSet>
        <FieldLegend variant="label">Tasks</FieldLegend>
        <FieldDescription>
          Get notified when tasks you&apos;ve created have updates.
        </FieldDescription>
        <FieldGroup data-slot="checkbox-group">
          {tasks.map((task) => (
            <Field
              key={task.id}
              orientation="horizontal"
              data-invalid={isInvalid}
            >
              <Checkbox
                id={`form-tanstack-checkbox-${task.id}`}
                name={field.name}
                aria-invalid={isInvalid}
                checked={field.state.value.includes(task.id)}
                onCheckedChange={(checked) => {
                  if (checked) {
                    field.pushValue(task.id)
                  } else {
                    const index = field.state.value.indexOf(task.id)
                    if (index > -1) {
                      field.removeValue(index)
                    }
                  }
                }}
              />
              <FieldLabel
                htmlFor={`form-tanstack-checkbox-${task.id}`}
                className="font-normal"
              >
                {task.label}
              </FieldLabel>
            </Field>
          ))}
        </FieldGroup>
        {isInvalid && <FieldError errors={field.state.meta.errors} />}
      </FieldSet>
    )
  }}
/>

单选按钮组

🌐 Radio Group

  • 对于单选按钮组,在 <RadioGroup /> 组件上使用 field.state.valuefield.handleChange
  • 要显示错误,请在 <RadioGroupItem /> 组件中添加 aria-invalid 属性,在 <Field /> 组件中添加 data-invalid 属性。
Subscription Plan
See pricing and features for each plan.
Plan

You can upgrade or downgrade your plan at any time.

"use client"

import { useForm } from "@tanstack/react-form"
form.tsx
<form.Field
  name="plan"
  children={(field) => {
    const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
    return (
      <FieldSet>
        <FieldLegend>Plan</FieldLegend>
        <FieldDescription>
          You can upgrade or downgrade your plan at any time.
        </FieldDescription>
        <RadioGroup
          name={field.name}
          value={field.state.value}
          onValueChange={field.handleChange}
        >
          {plans.map((plan) => (
            <FieldLabel
              key={plan.id}
              htmlFor={`form-tanstack-radiogroup-${plan.id}`}
            >
              <Field orientation="horizontal" data-invalid={isInvalid}>
                <FieldContent>
                  <FieldTitle>{plan.title}</FieldTitle>
                  <FieldDescription>{plan.description}</FieldDescription>
                </FieldContent>
                <RadioGroupItem
                  value={plan.id}
                  id={`form-tanstack-radiogroup-${plan.id}`}
                  aria-invalid={isInvalid}
                />
              </Field>
            </FieldLabel>
          ))}
        </RadioGroup>
        {isInvalid && <FieldError errors={field.state.meta.errors} />}
      </FieldSet>
    )
  }}
/>

切换

🌐 Switch

  • 对于开关,使用 <Switch /> 组件上的 field.state.valuefield.handleChange
  • 要显示错误,请在 <Switch /> 组件中添加 aria-invalid 属性,在 <Field /> 组件中添加 data-invalid 属性。
Security Settings
Manage your account security preferences.

Enable multi-factor authentication to secure your account.

"use client"

import { useForm } from "@tanstack/react-form"
form.tsx
<form.Field
  name="twoFactor"
  children={(field) => {
    const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
    return (
      <Field orientation="horizontal" data-invalid={isInvalid}>
        <FieldContent>
          <FieldLabel htmlFor="form-tanstack-switch-twoFactor">
            Multi-factor authentication
          </FieldLabel>
          <FieldDescription>
            Enable multi-factor authentication to secure your account.
          </FieldDescription>
          {isInvalid && <FieldError errors={field.state.meta.errors} />}
        </FieldContent>
        <Switch
          id="form-tanstack-switch-twoFactor"
          name={field.name}
          checked={field.state.value}
          onCheckedChange={field.handleChange}
          aria-invalid={isInvalid}
        />
      </Field>
    )
  }}
/>

复杂形式

🌐 Complex Forms

这是一个包含多个字段和验证的更复杂表单的示例。

🌐 Here is an example of a more complex form with multiple fields and validation.

Subscription Plan

Choose your subscription plan.

Choose how often you want to be billed.

Add-ons

Select additional features you'd like to include.

Advanced analytics and reporting

Automated daily backups

24/7 premium customer support

Receive email updates about your subscription

"use client"

import * as React from "react"

重置表单

🌐 Resetting the Form

使用 form.reset() 将表单重置为默认值。

🌐 Use form.reset() to reset the form to its default values.

<Button type="button" variant="outline" onClick={() => form.reset()}>
  Reset
</Button>

数组字段

🌐 Array Fields

TanStack Form 提供了强大的数组字段管理功能,使用 mode="array"。这允许你动态地添加、删除和更新数组项,并且支持完整的验证。

🌐 TanStack Form provides powerful array field management with mode="array". This allows you to dynamically add, remove, and update array items with full validation support.

Contact Emails
Manage your contact email addresses.
Email Addresses

Add up to 5 email addresses where we can contact you.

"use client"

import * as React from "react"

此示例演示了使用数组字段管理多个电子邮件地址。用户最多可以添加5个电子邮件地址,可以删除单个地址,每个地址都会独立验证。

🌐 This example demonstrates managing multiple email addresses with array fields. Users can add up to 5 email addresses, remove individual addresses, and each address is validated independently.

数组字段结构

🌐 Array Field Structure

在父字段上使用 mode="array" 以启用数组字段管理。

🌐 Use mode="array" on the parent field to enable array field management.

form.tsx
<form.Field
  name="emails"
  mode="array"
  children={(field) => {
    return (
      <FieldSet>
        <FieldLegend variant="label">Email Addresses</FieldLegend>
        <FieldDescription>
          Add up to 5 email addresses where we can contact you.
        </FieldDescription>
        <FieldGroup>
          {field.state.value.map((_, index) => (
            // Nested field for each array item
          ))}
        </FieldGroup>
      </FieldSet>
    )
  }}
/>

嵌套字段

🌐 Nested Fields

使用括号表示法访问数组中的单个项目:fieldName[index].propertyName。此示例使用 InputGroup 将删除按钮与输入框内联显示。

🌐 Access individual array items using bracket notation: fieldName[index].propertyName. This example uses InputGroup to display the remove button inline with the input.

form.tsx
<form.Field
  name={`emails[${index}].address`}
  children={(subField) => {
    const isSubFieldInvalid =
      subField.state.meta.isTouched && !subField.state.meta.isValid
    return (
      <Field orientation="horizontal" data-invalid={isSubFieldInvalid}>
        <FieldContent>
          <InputGroup>
            <InputGroupInput
              id={`form-tanstack-array-email-${index}`}
              name={subField.name}
              value={subField.state.value}
              onBlur={subField.handleBlur}
              onChange={(e) => subField.handleChange(e.target.value)}
              aria-invalid={isSubFieldInvalid}
              placeholder="name@example.com"
              type="email"
            />
            {field.state.value.length > 1 && (
              <InputGroupAddon align="inline-end">
                <InputGroupButton
                  type="button"
                  variant="ghost"
                  size="icon-xs"
                  onClick={() => field.removeValue(index)}
                  aria-label={`Remove email ${index + 1}`}
                >
                  <XIcon />
                </InputGroupButton>
              </InputGroupAddon>
            )}
          </InputGroup>
          {isSubFieldInvalid && (
            <FieldError errors={subField.state.meta.errors} />
          )}
        </FieldContent>
      </Field>
    )
  }}
/>

添加项目

🌐 Adding Items

使用 field.pushValue(item) 向数组字段添加项目。当数组达到最大长度时,你可以禁用该按钮。

🌐 Use field.pushValue(item) to add items to an array field. You can disable the button when the array reaches its maximum length.

form.tsx
<Button
  type="button"
  variant="outline"
  size="sm"
  onClick={() => field.pushValue({ address: "" })}
  disabled={field.state.value.length >= 5}
>
  Add Email Address
</Button>

移除条目

🌐 Removing Items

使用 field.removeValue(index) 从数组字段中移除项。你可以有条件地仅在有多于一项时显示移除按钮。

🌐 Use field.removeValue(index) to remove items from an array field. You can conditionally show the remove button only when there's more than one item.

form.tsx
{
  field.state.value.length > 1 && (
    <InputGroupButton
      onClick={() => field.removeValue(index)}
      aria-label={`Remove email ${index + 1}`}
    >
      <XIcon />
    </InputGroupButton>
  )
}

数组验证

🌐 Array Validation

使用 Zod 的数组方法验证数组字段。

🌐 Validate array fields using Zod's array methods.

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."),
})