Form 表单组件

<Form> 组件扩展了 HTML <form> 元素的功能,提供 预加载 加载界面 客户端导航提交以及渐进增强特性。

对于需要更新 URL 搜索参数的表单特别有用,它能减少实现上述功能所需的样板代码。

基础用法:

import Form from 'next/form'

export default function Page() {
  return (
    <Form action="/search">
      {/* 提交时,输入值会被附加到
          URL 中,例如 /search?query=abc */}
      <input name="query" />
      <button type="submit">Submit</button>
    </Form>
  )
}
import Form from 'next/form'

export default function Search() {
  return (
    <Form action="/search">
      {/* 提交时,输入值会被附加到
          URL 中,例如 /search?query=abc */}
      <input name="query" />
      <button type="submit">Submit</button>
    </Form>
  )
}

参考文档

<Form> 组件的行为取决于 action 属性接收的是 string 还是 function

  • action字符串时,<Form> 的行为类似于使用 GET 方法的原生 HTML 表单。表单数据会被编码为 URL 的搜索参数,提交表单时会导航到指定 URL。此外 Next.js 还会:
    • 在表单可见时预加载路径,这会预加载共享 UI(如 layout.jsloading.js),实现更快的导航
    • 提交表单时执行客户端导航而非整页刷新,保留共享 UI 和客户端状态
  • action函数(服务端操作)时,<Form> 的行为类似于 React 表单,在提交时执行该操作

action (字符串) 属性

action 为字符串时,<Form> 组件支持以下属性:

属性示例类型必填
actionaction="/search"string (URL 或相对路径)
replacereplace={false}boolean-
scrollscroll={true}boolean-
prefetchprefetch={true}boolean-
  • action:表单提交时要导航到的 URL 或路径
    • 空字符串 "" 会导航到相同路由但更新搜索参数
  • replace:替换当前历史记录而非在浏览器历史栈中添加新记录。默认为 false
  • scroll:控制导航时的滚动行为。默认为 true,即滚动到新路由顶部,并在前进/后退时保持滚动位置
  • prefetch:控制当表单在用户视口中可见时是否预加载路径。默认为 true

action (函数) 属性

action 为函数时,<Form> 组件支持以下属性:

属性示例类型必填
actionaction={myAction}function (服务端操作)
  • action:表单提交时要调用的服务端操作。详见 React 文档

须知:当 action 为函数时,replacescroll 属性会被忽略

注意事项

  • formAction:可用于 <button><input type="submit"> 字段来覆盖 action 属性。Next.js 会执行客户端导航,但此方法不支持预加载
    • 使用 basePath 时,必须在 formAction 路径中包含它。例如 formAction="/base-path/search"
  • key:不支持向字符串 action 传递 key 属性。如需触发重新渲染或执行变更,请考虑改用函数 action
  • onSubmit:可用于处理表单提交逻辑。但调用 event.preventDefault() 会覆盖 <Form> 的行为(如导航到指定 URL)
  • methodencTypetarget:不支持这些属性,因为它们会覆盖 <Form> 的行为
    • 类似地,formMethodformEncTypeformTarget 可用于覆盖 methodencTypetarget 属性,使用它们会回退到原生浏览器行为
    • 如需使用这些属性,请改用 HTML <form> 元素
  • <input type="file">:当 action 为字符串时使用此输入类型,会匹配浏览器行为,提交文件名而非文件对象

示例

导航到搜索结果页的搜索表单

通过将路径作为 action 传递,可以创建导航到搜索结果页的搜索表单:

import Form from 'next/form'

export default function Page() {
  return (
    <Form action="/search">
      <input name="query" />
      <button type="submit">Submit</button>
    </Form>
  )
}
import Form from 'next/form'

export default function Page() {
  return (
    <Form action="/search">
      <input name="query" />
      <button type="submit">Submit</button>
    </Form>
  )
}

当用户更新查询输入字段并提交表单时,表单数据会被编码为 URL 的搜索参数,例如 /search?query=abc

须知:如果向 action 传递空字符串 "",表单会导航到相同路由但更新搜索参数。

在结果页,可以使用 searchParams page.js 属性获取查询,并用它从外部源获取数据。

import { getSearchResults } from '@/lib/search'

export default async function SearchPage({
  searchParams,
}: {
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
  const results = await getSearchResults((await searchParams).query)

  return <div>...</div>
}
import { getSearchResults } from '@/lib/search'

export default async function SearchPage({ searchParams }) {
  const results = await getSearchResults((await searchParams).query)

  return <div>...</div>
}

<Form> 在用户视口中可见时,/search 页面的共享 UI(如 layout.jsloading.js)会被预加载。提交时,表单会立即导航到新路由,并在获取结果时显示加载界面。可以使用 loading.js 设计回退 UI:

export default function Loading() {
  return <div>Loading...</div>
}
export default function Loading() {
  return <div>Loading...</div>
}

为了应对共享 UI 尚未加载的情况,可以使用 useFormStatus 向用户显示即时反馈。

首先创建在表单挂起时显示加载状态的组件:

'use client'
import { useFormStatus } from 'react-dom'

export default function SearchButton() {
  const status = useFormStatus()
  return (
    <button type="submit">{status.pending ? '搜索中...' : '搜索'}</button>
  )
}
'use client'
import { useFormStatus } from 'react-dom'

export default function SearchButton() {
  const status = useFormStatus()
  return (
    <button type="submit">{status.pending ? '搜索中...' : '搜索'}</button>
  )
}

然后更新搜索表单页面以使用 SearchButton 组件:

import Form from 'next/form'
import { SearchButton } from '@/ui/search-button'

export default function Page() {
  return (
    <Form action="/search">
      <input name="query" />
      <SearchButton />
    </Form>
  )
}
import Form from 'next/form'
import { SearchButton } from '@/ui/search-button'

export default function Page() {
  return (
    <Form action="/search">
      <input name="query" />
      <SearchButton />
    </Form>
  )
}

使用服务端操作执行变更

可以通过向 action 属性传递函数来执行变更。

import Form from 'next/form'
import { createPost } from '@/posts/actions'

export default function Page() {
  return (
    <Form action={createPost}>
      <input name="title" />
      {/* ... */}
      <button type="submit">创建文章</button>
    </Form>
  )
}
import Form from 'next/form'
import { createPost } from '@/posts/actions'

export default function Page() {
  return (
    <Form action={createPost}>
      <input name="title" />
      {/* ... */}
      <button type="submit">创建文章</button>
    </Form>
  )
}

变更后通常会重定向到新资源。可以使用 next/navigationredirect 函数导航到新文章页。

须知:由于表单提交的"目标"在执行操作前未知,<Form> 无法自动预加载共享 UI。

'use server'
import { redirect } from 'next/navigation'

export async function createPost(formData: FormData) {
  // 创建新文章
  // ...

  // 重定向到新文章
  redirect(`/posts/${data.id}`)
}
'use server'
import { redirect } from 'next/navigation'

export async function createPost(formData) {
  // 创建新文章
  // ...

  // 重定向到新文章
  redirect(`/posts/${data.id}`)
}

然后在新页面中,可以使用 params 属性获取数据:

import { getPost } from '@/posts/data'

export default async function PostPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const data = await getPost(id)

  return (
    <div>
      <h1>{data.title}</h1>
      {/* ... */}
    </div>
  )
}
import { getPost } from '@/posts/data'

export default async function PostPage({ params }) {
  const { id } = await params
  const data = await getPost(id)

  return (
    <div>
      <h1>{data.title}</h1>
      {/* ... */}
    </div>
  )
}

更多示例请参阅服务端操作文档。