切换语言
切换主题

Next.js Server Actions 教程:表单处理与验证最佳实践

周五晚上十点半,你坐在电脑前,盯着一个用户注册表单的代码。文件夹里已经堆了四个文件:表单组件、API Route、类型定义、错误处理…你突然意识到,为了处理一个简单的表单提交,竟然写了快 200 行代码。

有没有更简单的方式?

答案是 Server Actions。这个 Next.js App Router 里的特性,能把表单处理流程简化 80%。不用写 API Route,不用手动 fetch,甚至不需要那些繁琐的状态管理。听起来很美好,但你可能也有疑问:这玩意真的安全吗?验证怎么做?Loading 状态咋处理?

说实话,我刚开始用的时候也有这些顾虑。用了几个月后,踩过坑也总结了些经验,想跟你聊聊 Next.js Server Actions 在表单处理中的实战技巧——从最基础的提交,到 Zod 验证、安全防护、用户体验优化,这篇文章会通过真实代码示例,让你快速上手这个特性。

Server Actions 基础

什么是 Server Actions?

Server Actions 就是运行在服务端的异步函数,你用 'use server' 标记它,然后就能直接在表单的 action 属性里使用。这样一来,表单提交时自动调用这个函数,数据处理、数据库操作、缓存更新…全在服务端完成。

关键特点是:

  • 类型安全:TypeScript 能检查到整个链路
  • 零配置:不用创建 /api 文件夹
  • 自动处理:FormData 自动传进来

两种写法,你可以把 Server Action 直接写在组件里(内联),也可以单独放一个文件(模块级):

// 方式1:内联在组件中
export default function Page() {
  async function createUser(formData: FormData) {
    'use server' // 标记为 Server Action
    const name = formData.get('name')
    // 处理数据...
  }

  return <form action={createUser}>...</form>
}

// 方式2:独立文件(推荐)
// app/actions.ts
'use server' // 文件级别标记

export async function createUser(formData: FormData) {
  const name = formData.get('name')
  // 处理数据...
}

你可能会问:Server Actions 和传统的 API Routes 有啥区别?啥时候该用哪个?

我整理了一张对比表:

特性Server ActionsAPI Routes
用途表单提交、数据变更RESTful API、外部调用
HTTP 方法只支持 POST支持 GET/POST/PUT/DELETE 等
类型安全天然类型安全需要手动定义类型
调用方式直接调用函数fetch 请求
适合场景内部逻辑、表单公开 API、第三方集成
代码量相对多

简单来说:内部用 Server Actions,对外用 API Routes。如果只是处理自己应用里的表单,Server Actions 够了。但要给其他系统提供接口、或者需要 GET 请求的话,还是得用 API Routes。

根据 Vercel 2025 年的调查,已经有 63% 的开发者在生产环境用上了 Server Actions。这不是什么实验性特性了。

"已经有 63% 的开发者在生产环境用上了 Server Actions"

第一个 Server Actions 示例

直接上代码,看个最简单的登录表单:

// app/login/page.tsx
export default function LoginPage() {
  async function handleLogin(formData: FormData) {
    'use server' // 标记为服务端函数

    // 从表单获取数据
    const email = formData.get('email') as string
    const password = formData.get('password') as string

    // 处理登录逻辑(这里简化演示)
    console.log('登录尝试:', email)

    // 实际项目中这里会验证用户、生成 token 等
  }

  return (
    <form action={handleLogin}>
      <input
        type="email"
        name="email"
        placeholder="邮箱"
        required
      />
      <input
        type="password"
        name="password"
        placeholder="密码"
        required
      />
      <button type="submit">登录</button>
    </form>
  )
}

就这么简单。关键点:

  1. 'use server':告诉 Next.js 这个函数要在服务端跑
  2. formData.get():用字段的 name 属性获取值
  3. action={handleLogin}:表单提交时自动调用

运行效果是:点击提交按钮,浏览器不会刷新页面,数据直接发到服务端处理。比传统方式少写了一堆 fetchuseState、错误处理…

但这只是最基础的。真实项目里,你还得做验证、显示错误、处理 Loading 状态。接着往下看。

表单验证实战

使用 Zod 做表单验证

只靠客户端的 required 属性?太天真了。用户随便打开个浏览器开发工具,就能绕过这些验证。服务端验证是必须的。

这就是 Zod 派上用场的地方。它能在服务端验证数据格式,发现问题立刻返回错误,防止脏数据进数据库。

先装 Zod:

npm install zod

然后定义验证规则:

// app/actions.ts
'use server'

import { z } from 'zod'

// 定义验证 schema
const SignupSchema = z.object({
  name: z.string().min(2, '姓名至少 2 个字符'),
  email: z.string().email('邮箱格式不对'),
  password: z.string().min(8, '密码至少 8 位'),
})

export async function signup(formData: FormData) {
  // 从 FormData 提取数据
  const rawData = {
    name: formData.get('name'),
    email: formData.get('email'),
    password: formData.get('password'),
  }

  // 验证数据
  const result = SignupSchema.safeParse(rawData)

  // 验证失败,返回错误
  if (!result.success) {
    return {
      success: false,
      errors: result.error.flatten().fieldErrors, // 字段级别的错误
    }
  }

  // 验证通过,处理业务逻辑
  const { name, email, password } = result.data

  // 创建用户、保存数据库等...
  console.log('创建用户:', { name, email })

  return {
    success: true,
    message: '注册成功!',
  }
}

关键点:

  1. safeParse 不会抛异常:失败时返回 { success: false, error: ... },你可以优雅地处理错误
  2. flatten().fieldErrors:把验证错误转成 { name: ['错误1'], email: ['错误2'] } 这种格式,方便展示
  3. 返回结构化数据:包含 success 标志和错误信息,客户端根据这个决定怎么展示

但现在还有个问题:怎么把这些错误显示在表单里?这就需要 useActionState 了。

展示验证错误:useActionState

useActionState 是 React 19 引入的 Hook(之前叫 useFormState),专门用来处理 Server Actions 返回的状态。它会:

  • 把服务端返回的数据保存到组件状态
  • 提供一个包装后的 action 函数
  • 告诉你表单是否正在提交

先看代码:

// app/signup/page.tsx
'use client' // 使用 Hook 需要标记为客户端组件

import { useActionState } from 'react'
import { signup } from '@/app/actions'

export default function SignupPage() {
  // 定义初始状态
  const initialState = { success: false, errors: {}, message: '' }

  // useActionState 接收:Server Action 和初始状态
  const [state, formAction, isPending] = useActionState(signup, initialState)

  return (
    <form action={formAction}> {/* 用 formAction 替代原始 action */}
      <div>
        <label>姓名</label>
        <input
          type="text"
          name="name"
          required
        />
        {/* 显示字段错误 */}
        {state.errors?.name && (
          <p className="error">{state.errors.name[0]}</p>
        )}
      </div>

      <div>
        <label>邮箱</label>
        <input
          type="email"
          name="email"
          required
        />
        {state.errors?.email && (
          <p className="error">{state.errors.email[0]}</p>
        )}
      </div>

      <div>
        <label>密码</label>
        <input
          type="password"
          name="password"
          required
        />
        {state.errors?.password && (
          <p className="error">{state.errors.password[0]}</p>
        )}
      </div>

      <button type="submit" disabled={isPending}>
        {isPending ? '提交中...' : '注册'}
      </button>

      {/* 显示成功消息 */}
      {state.success && (
        <p className="success">{state.message}</p>
      )}
    </form>
  )
}

工作流程是:

  1. 用户提交表单 → 调用 signup
  2. 服务端验证失败 → 返回 { success: false, errors: {...} }
  3. useActionState 把这个结果存到 state
  4. 组件重新渲染,显示错误信息

isPending 这个值在表单提交时为 true,完成后变 false,你可以用它来禁用按钮、显示 Loading 文字。

但你可能注意到了:用户填的内容在验证失败后会丢失。要保留表单数据,可以在返回时加上 values 字段,然后用 defaultValue 设置到输入框。这里就不展开了,重点是理解 useActionState 的作用:连接客户端组件和 Server Actions,让状态管理变简单

用户体验优化

Loading 状态与防重复提交

上面用了 isPending 来显示 Loading,但其实还有另一个 Hook:useFormStatus。这俩容易搞混,我一开始也懵过。

简单说:

  • useActionStateisPending:适合在表单组件里用
  • useFormStatuspending:适合在表单的子组件(比如提交按钮)里用

useFormStatus 有个限制:必须在 <form> 的子组件里调用,不能直接在表单组件用。这听起来麻烦,但好处是可以把按钮抽成独立组件复用。

看个例子,把提交按钮拆出来:

// components/SubmitButton.tsx
'use client'

import { useFormStatus } from 'react-dom'

export function SubmitButton({ children }: { children: React.ReactNode }) {
  const { pending } = useFormStatus() // 获取表单提交状态

  return (
    <button
      type="submit"
      disabled={pending}
      className={pending ? 'loading' : ''}
    >
      {pending ? '提交中...' : children}
    </button>
  )
}

然后在表单里直接用:

// app/signup/page.tsx
'use client'

import { useActionState } from 'react'
import { signup } from '@/app/actions'
import { SubmitButton } from '@/components/SubmitButton'

export default function SignupPage() {
  const [state, formAction] = useActionState(signup, { success: false, errors: {} })

  return (
    <form action={formAction}>
      {/* 表单字段... */}

      <SubmitButton>注册</SubmitButton> {/* 自动处理 Loading */}

      {state.errors?.general && (
        <p className="error">{state.errors.general}</p>
      )}
    </form>
  )
}

这样一来,按钮的 Loading 逻辑完全封装好了。在提交时:

  • 按钮自动禁用,防止重复提交
  • 文字变成”提交中…”
  • 可以加个转圈动画

pendingisPending 的区别?

特性useActionStateisPendinguseFormStatuspending
调用位置表单组件内部表单的子组件内部
适用场景需要访问表单整体状态只关心提交状态的独立按钮
灵活性可以同时获取 state 和 pending只能获取 pending

实际项目里,我一般这样用:

  • 表单逻辑复杂、需要处理多种状态 → 用 useActionState
  • 只是做个通用的提交按钮 → 用 useFormStatus

渐进式增强

有个挺酷的特性:Server Actions 支持渐进式增强。啥意思?就是即使用户浏览器禁用了 JavaScript,表单依然能提交。

这是因为 Server Actions 本质上还是利用浏览器原生的 <form> 提交机制。Next.js 在有 JavaScript 的情况下会劫持提交过程,做成 AJAX 请求;没 JavaScript 时,就退化成传统的表单提交。

实际应用场景?老实说不多。现在哪个网站没 JavaScript 还能用…但对无障碍访问、爬虫友好性来说,这是个加分项。而且你啥都不用做,Next.js 自动搞定。

安全性与最佳实践

Server Actions 的安全性

这是最容易被忽视的部分。很多人以为 Server Actions 运行在服务端,就自动安全了。大错特错

Server Actions 本质上就是公开的 API 端点。虽然 Next.js 给它生成了一个难猜的 ID,但这只是”模糊化”,不是真正的安全措施。懂点技术的人,打开浏览器开发工具看看网络请求,就能找到这个 Action 的 ID,然后手动调用它。

Next.js 提供了一些内置保护:

  1. CSRF 防护:Server Actions 只能通过 POST 请求调用,且会检查 Origin 和 Host 头是否匹配。跨站请求会被拒绝。
  2. 安全的 Action ID:每个 Action 都有个加密的 ID,不容易被穷举。
  3. 闭包变量加密:如果你在 Action 里用了外部变量,Next.js 会加密它们。

但这些远远不够。你必须做这些事

1. 输入验证

永远不要信任客户端数据。前面讲过用 Zod 验证,这是必须的。

2. 身份认证

检查用户是否登录。每个需要权限的 Action,都要验证身份。

3. 权限验证

登录不等于有权限。比如用户 A 不能删除用户 B 的数据,要验证操作权限。

看个实际例子:

// app/actions.ts
'use server'

import { cookies } from 'next/headers'
import { z } from 'zod'

const DeletePostSchema = z.object({
  postId: z.string().min(1),
})

export async function deletePost(formData: FormData) {
  // 1. 验证输入
  const rawData = {
    postId: formData.get('postId'),
  }

  const result = DeletePostSchema.safeParse(rawData)
  if (!result.success) {
    return { success: false, error: '无效的请求' }
  }

  const { postId } = result.data

  // 2. 身份认证:检查用户是否登录
  const cookieStore = await cookies()
  const sessionToken = cookieStore.get('session')?.value

  if (!sessionToken) {
    return { success: false, error: '请先登录' }
  }

  // 3. 获取当前用户
  const currentUser = await getUserFromSession(sessionToken)
  if (!currentUser) {
    return { success: false, error: '会话已过期' }
  }

  // 4. 权限验证:检查这篇文章是否属于当前用户
  const post = await getPost(postId)
  if (!post) {
    return { success: false, error: '文章不存在' }
  }

  if (post.authorId !== currentUser.id) {
    return { success: false, error: '你没有权限删除这篇文章' }
  }

  // 5. 执行操作
  await deletePostFromDB(postId)

  return { success: true, message: '删除成功' }
}

这个例子演示了完整的安全检查流程:输入验证 → 身份认证 → 权限验证 → 执行操作。缺一不可。

还有个实用工具推荐:next-safe-action 库。它提供了中间件机制,可以统一处理验证、认证、错误处理:

import { createSafeActionClient } from 'next-safe-action'

// 创建一个带认证的 action 客户端
const actionClient = createSafeActionClient({
  // 中间件:检查用户登录状态
  async middleware() {
    const session = await getSession()
    if (!session) {
      throw new Error('未登录')
    }
    return { userId: session.userId }
  },
})

// 使用时自动带上认证检查
export const deletePost = actionClient
  .schema(DeletePostSchema)
  .action(async ({ parsedInput, ctx }) => {
    const { postId } = parsedInput
    const { userId } = ctx // 从中间件获取用户 ID

    // 执行删除...
  })

这样一来,所有需要认证的 Action 都复用同一套逻辑,代码清爽多了。

记住:Server Actions 不是黑魔法,它就是个 API 端点。该做的安全措施一个都不能少。

实战案例:带身份验证的表单

来个完整的例子,做个只有登录用户才能提交的评论表单:

// app/actions.ts
'use server'

import { cookies } from 'next/headers'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'

const CommentSchema = z.object({
  postId: z.string(),
  content: z.string().min(1, '评论不能为空').max(500, '评论最多 500 字'),
})

export async function addComment(formData: FormData) {
  // 1. 验证输入
  const rawData = {
    postId: formData.get('postId'),
    content: formData.get('content'),
  }

  const result = CommentSchema.safeParse(rawData)
  if (!result.success) {
    return {
      success: false,
      errors: result.error.flatten().fieldErrors,
    }
  }

  // 2. 身份认证
  const cookieStore = await cookies()
  const sessionToken = cookieStore.get('session')?.value

  if (!sessionToken) {
    return {
      success: false,
      error: '请先登录后再评论',
    }
  }

  const user = await getUserFromSession(sessionToken)
  if (!user) {
    return {
      success: false,
      error: '会话已过期,请重新登录',
    }
  }

  // 3. 保存评论
  const { postId, content } = result.data

  await saveComment({
    postId,
    content,
    authorId: user.id,
    authorName: user.name,
    createdAt: new Date(),
  })

  // 4. 重新验证页面缓存,让评论立即显示
  revalidatePath(`/posts/${postId}`)

  return {
    success: true,
    message: '评论成功',
  }
}

客户端组件:

// app/posts/[id]/CommentForm.tsx
'use client'

import { useActionState } from 'react'
import { addComment } from '@/app/actions'
import { SubmitButton } from '@/components/SubmitButton'

export function CommentForm({ postId }: { postId: string }) {
  const [state, formAction] = useActionState(addComment, {
    success: false,
    errors: {},
  })

  return (
    <form action={formAction}>
      {/* 隐藏字段传递 postId */}
      <input type="hidden" name="postId" value={postId} />

      <textarea
        name="content"
        placeholder="写下你的评论..."
        rows={4}
        required
      />

      {state.errors?.content && (
        <p className="error">{state.errors.content[0]}</p>
      )}

      {state.error && (
        <p className="error">{state.error}</p>
      )}

      {state.success && (
        <p className="success">{state.message}</p>
      )}

      <SubmitButton>发表评论</SubmitButton>
    </form>
  )
}

这个例子结合了前面讲的所有要点:

  • Zod 验证输入
  • 检查用户登录状态
  • 使用 useActionState 处理状态
  • revalidatePath 刷新缓存
  • 提交按钮带 Loading 状态

完整的表单处理流程,生产可用。

进阶技巧

传递额外参数

有时候你需要传递表单字段之外的参数。比如编辑文章时,除了表单内容,还要传文章 ID。

一个办法是用隐藏字段:

<input type="hidden" name="postId" value={postId} />

但还有更优雅的方式:用 JavaScript 的 bind 方法。

// app/actions.ts
'use server'

export async function updatePost(postId: string, formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string

  // 更新文章...
  await updatePostInDB(postId, { title, content })

  return { success: true }
}

客户端调用时:

// app/posts/[id]/edit/page.tsx
'use client'

import { updatePost } from '@/app/actions'

export default function EditPost({ postId }: { postId: string }) {
  // 用 bind 绑定 postId 参数
  const updatePostWithId = updatePost.bind(null, postId)

  return (
    <form action={updatePostWithId}>
      <input type="text" name="title" required />
      <textarea name="content" required />
      <button type="submit">更新</button>
    </form>
  )
}

bind(null, postId) 的作用是创建一个新函数,把 postId 固定为第一个参数。这样表单提交时,FormData 会作为第二个参数传进去。

适用场景:编辑、删除等需要传 ID 的操作。

数据重新验证

Server Actions 处理完数据后,相关页面的缓存可能已经过时了。Next.js 提供了两个函数来刷新缓存:

1. revalidatePath

按路径刷新:

import { revalidatePath } from 'next/cache'

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

  // 刷新首页的文章列表
  revalidatePath('/')
  // 刷新文章详情页
  revalidatePath(`/posts/${newPostId}`)

  return { success: true }
}

2. revalidateTag

按标签刷新(需要先在 fetch 时打标签):

// 获取数据时打标签
fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] }
})

// Server Action 里刷新所有带 'posts' 标签的缓存
import { revalidateTag } from 'next/cache'

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

  revalidateTag('posts') // 刷新所有相关缓存

  return { success: true }
}

啥时候用哪个?

  • 路径固定、数量少 → 用 revalidatePath
  • 数据分散在多个页面 → 用 revalidateTag

我一般优先用 revalidatePath,简单直接。只有在一个操作影响很多页面时,才考虑用标签。

乐观更新

有些操作几乎不会失败,比如点赞、收藏。这种情况下,可以用乐观更新:先在 UI 上显示成功,后台再慢慢提交。

React 19 提供了 useOptimistic Hook:

'use client'

import { useOptimistic } from 'react'
import { likePost } from '@/app/actions'

export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
  const [optimisticLikes, setOptimisticLikes] = useOptimistic(initialLikes)

  async function handleLike() {
    // 立即更新 UI(乐观)
    setOptimisticLikes(optimisticLikes + 1)

    // 后台提交
    await likePost(postId)
  }

  return (
    <button onClick={handleLike}>
      👍 {optimisticLikes}
    </button>
  )
}

用户点击按钮,数字立刻 +1,不用等服务端响应。体验丝滑。

但要注意:只在成功率极高的操作用这个。如果失败了,还得回滚 UI,反而更麻烦。

结论

说了这么多,总结三点:

  1. Server Actions 简化了表单处理,但不是万能的。内部表单用它,对外 API 还是得用 Route Handlers。别一股脑全用 Server Actions。

  2. 安全性要自己保障。框架提供的只是基础防护,输入验证、身份认证、权限检查…该做的一个都不能少。别指望 Next.js 帮你搞定一切。

  3. 用户体验细节很重要。Loading 状态、错误提示、乐观更新…这些小细节决定了用户觉得你的应用是”还行”还是”真好用”。结合 useActionStateuseFormStatus,把这些都处理好。

从最简单的表单开始试试吧。创个 Server Action,加上 Zod 验证,显示个 Loading,你就掌握 80% 的用法了。剩下的 20%(缓存刷新、乐观更新等),等用到的时候再查官方文档。

Next.js 和 React 都在快速迭代,Server Actions 的 API 可能还会变。记得关注官方文档的更新,别让这篇文章里的代码过时太快。

现在就去你的项目里试试吧。下次写表单提交的时候,也许你会发现,原来可以这么简单。

使用 Server Actions 处理表单的完整流程

从创建 Server Action 到添加验证、处理状态的完整步骤

⏱️ 预计耗时: 30 分钟

  1. 1

    步骤1: 创建 Server Action

    在 app/actions.ts 文件中创建 Server Action:

    1. 文件级别标记:在文件顶部添加 'use server'
    2. 定义函数:export async function actionName(formData: FormData)
    3. 获取数据:使用 formData.get('fieldName') 获取表单字段
    4. 返回结果:返回 { success: boolean, errors?: {}, message?: string } 格式

    示例:
    ```typescript
    'use server'
    export async function signup(formData: FormData) {
    const name = formData.get('name') as string
    // 处理逻辑...
    return { success: true, message: '注册成功' }
    }
    ```
  2. 2

    步骤2: 添加 Zod 验证

    使用 Zod 进行服务端数据验证:

    1. 安装 Zod:npm install zod
    2. 定义 Schema:const SignupSchema = z.object({ name: z.string().min(2), email: z.string().email() })
    3. 验证数据:const result = SignupSchema.safeParse(rawData)
    4. 处理错误:if (!result.success) return { success: false, errors: result.error.flatten().fieldErrors }

    关键点:
    • safeParse 不会抛异常,返回 { success, data/error }
    • flatten().fieldErrors 将错误转为 { field: ['error1'] } 格式
    • 验证失败时返回结构化错误,客户端可展示
  3. 3

    步骤3: 使用 useActionState 处理状态

    在客户端组件中使用 useActionState:

    1. 导入 Hook:import { useActionState } from 'react'
    2. 定义初始状态:const initialState = { success: false, errors: {} }
    3. 使用 Hook:const [state, formAction, isPending] = useActionState(action, initialState)
    4. 绑定表单:<form action={formAction}>
    5. 显示错误:{state.errors?.field && <p>{state.errors.field[0]}</p>}
    6. 显示 Loading:<button disabled={isPending}>{isPending ? '提交中...' : '提交'}</button>

    工作流程:
    • 用户提交 → 调用 action → 返回结果 → state 更新 → 组件重新渲染
  4. 4

    步骤4: 添加身份认证和权限验证

    在 Server Action 中添加安全检查:

    1. 输入验证:使用 Zod 验证所有输入
    2. 身份认证:检查 session token
    ```typescript
    const cookieStore = await cookies()
    const sessionToken = cookieStore.get('session')?.value
    if (!sessionToken) return { success: false, error: '请先登录' }
    ```
    3. 权限验证:检查操作权限
    ```typescript
    const post = await getPost(postId)
    if (post.authorId !== currentUser.id) {
    return { success: false, error: '无权限' }
    }
    ```
    4. 执行操作:验证通过后执行实际业务逻辑

    记住:Server Actions 不是黑魔法,必须手动做安全检查
  5. 5

    步骤5: 优化用户体验

    添加 Loading 状态和错误处理:

    1. 使用 useFormStatus(在按钮组件中):
    ```typescript
    'use client'
    import { useFormStatus } from 'react-dom'
    export function SubmitButton() {
    const { pending } = useFormStatus()
    return <button disabled={pending}>...</button>
    }
    ```
    2. 使用 revalidatePath 刷新缓存:
    ```typescript
    import { revalidatePath } from 'next/cache'
    revalidatePath('/posts')
    ```
    3. 乐观更新(可选,用于高成功率操作):
    ```typescript
    const [optimisticState, setOptimisticState] = useOptimistic(initialState)
    ```

    最佳实践:
    • 表单逻辑复杂 → 用 useActionState
    • 独立按钮组件 → 用 useFormStatus
    • 操作成功后 → 刷新相关页面缓存

常见问题

Server Actions 和 API Routes 有什么区别?什么时候用哪个?
Server Actions 适合内部表单提交和数据变更,只支持 POST,类型安全,代码量少。API Routes 适合对外提供 RESTful API、需要 GET 请求、第三方集成等场景。简单来说:内部用 Server Actions,对外用 API Routes。
Server Actions 安全吗?需要做哪些安全措施?
Server Actions 运行在服务端,但必须手动做安全检查:1) 输入验证(使用 Zod),2) 身份认证(检查 session),3) 权限验证(检查操作权限)。框架只提供基础 CSRF 防护,不能依赖框架自动保障安全。
useActionState 和 useFormStatus 的区别是什么?
useActionState 的 isPending 适合在表单组件内部使用,可以同时获取 state 和 pending。useFormStatus 的 pending 必须在表单的子组件(如按钮)中使用,只能获取 pending 状态。表单逻辑复杂用 useActionState,独立按钮组件用 useFormStatus。
如何传递表单字段之外的参数?
有两种方式:1) 使用隐藏字段 <input type="hidden" name="postId" value={postId} />,2) 使用 bind 方法:const actionWithId = action.bind(null, postId),然后 <form action={actionWithId}>。推荐使用 bind 方法,更优雅。
表单提交后如何刷新页面数据?
使用 revalidatePath 按路径刷新:revalidatePath('/posts'),或使用 revalidateTag 按标签刷新(需要先在 fetch 时打标签)。路径固定、数量少用 revalidatePath,数据分散在多个页面用 revalidateTag。
什么时候使用乐观更新?
乐观更新适合成功率极高的操作(如点赞、收藏),使用 useOptimistic Hook 立即更新 UI,后台再提交。如果操作可能失败,不建议使用,因为失败后需要回滚 UI,反而更麻烦。

12 分钟阅读 · 发布于: 2025年12月19日 · 修改于: 2026年1月22日

评论

使用 GitHub 账号登录后即可评论

相关文章