Server Actions —— 前后端交互的未来形态
概述
Server Actions 是可以直接从客户端代码(尤其是 Client Components)中调用的异步函数,但它们的函数体本身只在服务器上执行。
在这种模式下,你不会在浏览器 DevTools 的 Network 面板中看到任何请求,因为请求由服务器帮你完成了。这个看起来是多加了一层服务器在中间转发,不过也会带来几项优点:
- 服务器会自动缓存,大量客户端使用同一份数据的时候,不会多次请求;
- 发给客户端的都是已经渲染好的页面,这对 SEO 极其有利;
- 这种模式下,我们甚至可以直接操作数据库一步完成变更或者执行其他副作用,这在利用 NextJS 直接全栈开发的情况下尤其有利;
- 在某些禁用 JS 的情况下,它的渐进增强的性质使得出乎意料的仍能工作。
本质
它们是建立在 Web Fundamentals(特别是 HTML <form>)之上的一个 RPC (远程过程调用) 的抽象。你感觉像是在调用一个本地函数,但 Next.js 在底层为你处理了网络请求、数据序列化和反序列化。
优势
- 零 API 路由: 你不再需要为了一个简单的创建或更新操作去手动创建
/api/目录下的文件。你的业务逻辑和数据变更函数可以放在任何地方(通常是和服务端代码放在一起)。 - 简化数据变更: 你可以直接在 Server Action 内部调用数据库或服务层函数,修改数据,然后告诉 Next.js 哪些数据需要刷新。
- 无缝的数据刷新: 通过
revalidatePath或revalidateTag,你可以精确地让 Next.js 重新获取特定页面的数据并更新 UI,无需客户端手动 refetch。 - 渐进增强 (Progressive Enhancement): 这是一个非常优雅的特性。如果用户的浏览器禁用了 JavaScript,一个绑定了 Server Action 的
<form>仍然可以作为标准 HTML 表单工作,实现全页刷新提交。当 JavaScript 可用时,Next.js 会接管它,实现无刷新的局部更新。
实战演示
定义 action 函数
我会用一个简单的 Todo List 来演示怎么使用。
先定义一个用 fetch 封装的请求函数,这个通常会在 @/services 文件夹里,但是我采用 Ducks 风格组织代码,所以:
src\features\todos\todoServices.ts :
1 | export const addTodo = async (content: string) => { |
接着,我来创建真正用于 server action 的函数。这个函数,本质上就是基于上面的函数多一层封装。
这个封装出来的函数,能直接用于 <form action = { ... } > 标签的 action 属性。
众所周知,如果直接在 action 属性里填写 URL,HTML 的默认行为会自己请求这个地址;然而,React 和 NextJS 对这个行为进行了原生的增强。
现在,你可以往里面传一个函数,React 还会自动把 form 表单里的数据填入这个函数,作为其形参。我们现在就来定义这个函数:
src\features\todos\todoActions.ts
1 | export const createTodo = async ( |
核心就 3 点:
- 函数的形参是表单数据,React 自动填入。
- 函数定义处用了 use server,因为这个函数是在服务端执行
- 完成之后要
revalidatePath("..."),当我们调用它时,Next.js 会做两件事:- 清除服务端的数据缓存: 它会使 getTodos 在下次被调用时,必须重新 fetch 数据,而不是使用缓存
- 触发 RSC 重新渲染: Next.js 知道 / 路径的数据“脏了”,它会重新在服务器上运行 Home 这个 Server Component。Home 内部会再次调用 getTodos,获取到包含了新 Todo 的列表,然后将新的 RSC Payload 发送给浏览器。浏览器端的 React 会高效地 diff 并更新 DOM,于是新添加的 Todo 就出现在了页面上!
这个函数目前还出奇的简单,其实我们可以在里面加一些服务端可以做的事情,比如验证之类的,下文中我会体现。
使用 action 函数
简单到难以置信,这里直接贴代码了
1 | // components/add-todo-form.tsx |
我们没有写一行 fetch 的客户端代码,没有写一行 useState 来管理列表状态,没有写任何 useEffect 来同步数据,就实现了这个核心功能。这就是 Server Actions 的强大之处。
使用 useActionState 和 useFormStatus HOOK 进一步增强
类似 TanStack Query 以及 React Form Hook 都有自己的 state 机制,很多时候在进行用户提示或者状态管理的时候会感到无从下手,我到底该用谁?!
请注意,本文中所有的校验、状态等,都是在反应服务器端的情况,因为这是配合 server action 用的。
在客户端可以进行一次校验,服务端再进行一次校验。state 作用的层级和位置是不同的,需要区分。
这一切很现代,很棒,但是还并没有友好的状态管理和错误处理机制。
下一步,我们需要使用两个 hook,这两个是 server action 的最佳搭档,我们的目标是在用户执行 add todo 成功或者失败的时候,都能用 toast 弹出一条友好的提示。
使用 useFormStatus hook
一个有趣的事实:
useFormStatus 现在是 “React DOM” 包里唯一一个 hook 了。
useFormStatus 的用法很简单,我直接用一个 submit button 举例:
1 | "use client"; |
其实就是从中解构出一个 pending 属性,这个在处理 loading 状态的时候很有用。
注意点:
useFormStatus hook 要求使用其的组件在 form 标签里
这个 hook 能很方便的创建出高度可复用的提交按钮这样的组件,而不依赖父组件表单的状态管理。
使用 useActionState hook
在早期的 React Canary 版本中,此 API 是 React DOM 的一部分,称为
useFormState,可见:useActionState – React 中文文档
useActionState 的签名大约长这样:
1 | const [state, action, isPending] = useActionState( |
其中的 actionFunction 其实就是我们上面定义的 server action 函数,而解构出的 action 是一个增强过的函数,我们需要把它替换掉原来的 actionFunction 给 form 表单用。
这个 hook 本质是为 server action 提供一个状态机,其解构出的另外两样东西也很有用:
-
state 结构由我们自己定义,useActionState 会自动帮我们管理这个 state ,接下来我会详细说这件事;
-
isPending 属性的作用其实和上面 useFormStatus 解构出来的的 pending 没啥区别,但是为了抽象出可复用的组件,我宁愿用 useFormStatus。
定义好 state 结构
要管理 state,我们得先有 state
src\features\todos\todoActions.ts
1 | export interface FormState { |
其中最主要就是 type 和 message 字段,前者标识状态类型,这个用于给用户提供友好的提示;
fieldErrors 用格式化输出详细报错信息,这个是给 zod 用的,能优雅的在出错时精确返回是哪一条出问题了。
formValues 用于数据的回填,想象一下用户提交信息失败了,这可能是因为网络原因之类的,用户并不想重新输入一次信息,所以你得把它出错的信息回填进去。
只在提交成功的时候手动清空表单,不就可以了吗?为什么要多此一举
这是因为提交后清空表单数据是 HTML 元素的默认行为。通常,我们会阻止此默认行为,但是由于 server action 本身又是利用了其默认行为,所以这里会很矛盾,显得不怎么优雅。不过目前来看,手动回填只能是不得已的方案了。
修改 server action 函数,增加返回值为 state
下面是完整的,修改好的 server action 函数。值得注意的是,这次我们设置了
- prevState 形参
- 以 state 为结构的返回值
提供给 useActionState 使用。
代码里还多了一些 zod 的操作,请注意,此时 zod 的验证发生在服务端,因为这是服务端函数。这恰恰体现了 zod 的强大:双端同步校验。
src\features\todos\todoActions.ts
1 | export const createTodo = async ( |
在组件里使用 useActionState hook
在这里,我们利用 useEffect 消费 useActionState 返回的 state 信息,来实现我们需要的友好的用户提示功能。
1 | const initialState: FormState = { |
- 标题: Server Actions —— 前后端交互的未来形态
- 作者: 三葉Leaves
- 创建于 : 2025-09-05 00:00:00
- 更新于 : 2026-03-16 12:05:06
- 链接: https://blog.oksanye.com/fbbe337a1649/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。